diff --git a/BinaryObjectScanner.Compression/BinaryObjectScanner.Compression.csproj b/BinaryObjectScanner.Compression/BinaryObjectScanner.Compression.csproj index 0c7b1816..de312edf 100644 --- a/BinaryObjectScanner.Compression/BinaryObjectScanner.Compression.csproj +++ b/BinaryObjectScanner.Compression/BinaryObjectScanner.Compression.csproj @@ -35,7 +35,7 @@ - + diff --git a/BinaryObjectScanner.Printing/BinaryObjectScanner.Printing.csproj b/BinaryObjectScanner.Printing/BinaryObjectScanner.Printing.csproj index 327828d7..34223c83 100644 --- a/BinaryObjectScanner.Printing/BinaryObjectScanner.Printing.csproj +++ b/BinaryObjectScanner.Printing/BinaryObjectScanner.Printing.csproj @@ -26,8 +26,8 @@ - - + + diff --git a/BinaryObjectScanner.Wrappers/AACSMediaKeyBlock.cs b/BinaryObjectScanner.Wrappers/AACSMediaKeyBlock.cs deleted file mode 100644 index 2acb9377..00000000 --- a/BinaryObjectScanner.Wrappers/AACSMediaKeyBlock.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.IO; -using System.Text; -using SabreTools.Models.AACS; - -namespace BinaryObjectScanner.Wrappers -{ - public class AACSMediaKeyBlock : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "AACS Media Key Block"; - - #endregion - - #region Constructors - - /// -#if NET48 - public AACSMediaKeyBlock(MediaKeyBlock model, byte[] data, int offset) -#else - public AACSMediaKeyBlock(MediaKeyBlock? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public AACSMediaKeyBlock(MediaKeyBlock model, Stream data) -#else - public AACSMediaKeyBlock(MediaKeyBlock? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create an AACS media key block from a byte array and offset - /// - /// Byte array representing the archive - /// Offset within the array to parse - /// An AACS media key block wrapper on success, null on failure -#if NET48 - public static AACSMediaKeyBlock Create(byte[] data, int offset) -#else - public static AACSMediaKeyBlock? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create an AACS media key block from a Stream - /// - /// Stream representing the archive - /// An AACS media key block wrapper on success, null on failure -#if NET48 - public static AACSMediaKeyBlock Create(Stream data) -#else - public static AACSMediaKeyBlock? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var mediaKeyBlock = new SabreTools.Serialization.Streams.AACS().Deserialize(data); - if (mediaKeyBlock == null) - return null; - - try - { - return new AACSMediaKeyBlock(mediaKeyBlock, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.AACSMediaKeyBlock.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/BDPlusSVM.cs b/BinaryObjectScanner.Wrappers/BDPlusSVM.cs deleted file mode 100644 index f886e496..00000000 --- a/BinaryObjectScanner.Wrappers/BDPlusSVM.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.IO; -using System.Text; -using SabreTools.Models.BDPlus; - -namespace BinaryObjectScanner.Wrappers -{ - public class BDPlusSVM : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "BD+ SVM"; - - #endregion - - #region Constructors - - /// -#if NET48 - public BDPlusSVM(SVM model, byte[] data, int offset) -#else - public BDPlusSVM(SVM? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public BDPlusSVM(SVM model, Stream data) -#else - public BDPlusSVM(SVM? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a BD+ SVM from a byte array and offset - /// - /// Byte array representing the archive - /// Offset within the array to parse - /// A BD+ SVM wrapper on success, null on failure -#if NET48 - public static BDPlusSVM Create(byte[] data, int offset) -#else - public static BDPlusSVM? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a BD+ SVM from a Stream - /// - /// Stream representing the archive - /// A BD+ SVM wrapper on success, null on failure -#if NET48 - public static BDPlusSVM Create(Stream data) -#else - public static BDPlusSVM? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var svm = new SabreTools.Serialization.Streams.BDPlus().Deserialize(data); - if (svm == null) - return null; - - try - { - return new BDPlusSVM(svm, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.BDPlusSVM.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/BFPK.cs b/BinaryObjectScanner.Wrappers/BFPK.cs deleted file mode 100644 index 996d20b7..00000000 --- a/BinaryObjectScanner.Wrappers/BFPK.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System.IO; -using System.Text; -using SharpCompress.Compressors; -using SharpCompress.Compressors.Deflate; - -namespace BinaryObjectScanner.Wrappers -{ - public class BFPK : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "BFPK Archive"; - - #endregion - - #region Constructors - - /// -#if NET48 - public BFPK(SabreTools.Models.BFPK.Archive model, byte[] data, int offset) -#else - public BFPK(SabreTools.Models.BFPK.Archive? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public BFPK(SabreTools.Models.BFPK.Archive model, Stream data) -#else - public BFPK(SabreTools.Models.BFPK.Archive? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a BFPK archive from a byte array and offset - /// - /// Byte array representing the archive - /// Offset within the array to parse - /// A BFPK archive wrapper on success, null on failure -#if NET48 - public static BFPK Create(byte[] data, int offset) -#else - public static BFPK? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a BFPK archive from a Stream - /// - /// Stream representing the archive - /// A BFPK archive wrapper on success, null on failure -#if NET48 - public static BFPK Create(Stream data) -#else - public static BFPK? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var archive = new SabreTools.Serialization.Streams.BFPK().Deserialize(data); - if (archive == null) - return null; - - try - { - return new BFPK(archive, data); - } - catch - { - return null; - } - } - - #endregion - - #region Data - - /// - /// Extract all files from the BFPK to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public bool ExtractAll(string outputDirectory) - { - // If we have no files - if (this.Model.Files == null || this.Model.Files.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.Files.Length; i++) - { - allExtracted &= 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 bool ExtractFile(int index, string outputDirectory) - { - // If we have no files - if (this.Model.Files == null || this.Model.Files.Length == 0) - return false; - - // If we have an invalid index - if (index < 0 || index >= this.Model.Files.Length) - return false; - - // Get the file information - var file = this.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 > 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 = ReadFromDataSource(offset, compressedSize); -#else - byte[]? data = 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 Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.BFPK.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/BSP.cs b/BinaryObjectScanner.Wrappers/BSP.cs deleted file mode 100644 index d57f4b30..00000000 --- a/BinaryObjectScanner.Wrappers/BSP.cs +++ /dev/null @@ -1,389 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using static SabreTools.Models.BSP.Constants; - -namespace BinaryObjectScanner.Wrappers -{ - public class BSP : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Half-Life Level (BSP)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public BSP(SabreTools.Models.BSP.File model, byte[] data, int offset) -#else - public BSP(SabreTools.Models.BSP.File? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public BSP(SabreTools.Models.BSP.File model, Stream data) -#else - public BSP(SabreTools.Models.BSP.File? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a BSP from a byte array and offset - /// - /// Byte array representing the BSP - /// Offset within the array to parse - /// A BSP wrapper on success, null on failure -#if NET48 - public static BSP Create(byte[] data, int offset) -#else - public static BSP? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a BSP from a Stream - /// - /// Stream representing the BSP - /// An BSP wrapper on success, null on failure -#if NET48 - public static BSP Create(Stream data) -#else - public static BSP? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var file = new SabreTools.Serialization.Streams.BSP().Deserialize(data); - if (file == null) - return null; - - try - { - return new BSP(file, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.BSP.Print(builder, this.Model); - return builder; - } - - #endregion - - #region Extraction - - /// - /// Extract all lumps from the BSP to an output directory - /// - /// Output directory to write to - /// True if all lumps extracted, false otherwise - public bool ExtractAllLumps(string outputDirectory) - { - // If we have no lumps - if (this.Model.Lumps == null || this.Model.Lumps.Length == 0) - return false; - - // Loop through and extract all lumps to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.Lumps.Length; i++) - { - allExtracted &= 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 bool ExtractLump(int index, string outputDirectory) - { - // If we have no lumps - if (this.Model.Lumps == null || this.Model.Lumps.Length == 0) - return false; - - // If the lumps index is invalid - if (index < 0 || index >= this.Model.Lumps.Length) - return false; - - // Get the lump - var lump = this.Model.Lumps[index]; - if (lump == null) - return false; - - // Read the data -#if NET48 - byte[] data = ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#else - byte[]? data = ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#endif - if (data == null) - return false; - - // Create the filename - string filename = $"lump_{index}.bin"; - switch (index) - { - case HL_BSP_LUMP_ENTITIES: - filename = "entities.ent"; - break; - case 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 bool ExtractAllTextures(string outputDirectory) - { - // If we have no textures - if (this.Model.TextureHeader?.Offsets == null || this.Model.TextureHeader.Offsets.Length == 0) - return false; - - // Loop through and extract all lumps to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.TextureHeader.Offsets.Length; i++) - { - allExtracted &= 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 bool ExtractTexture(int index, string outputDirectory) - { - // If we have no textures - if (this.Model.Textures == null || this.Model.Textures.Length == 0) - return false; - - // If the texture index is invalid - if (index < 0 || index >= this.Model.Textures.Length) - return false; - - // Get the texture - var texture = this.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 - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/BinaryObjectScanner.Wrappers.csproj b/BinaryObjectScanner.Wrappers/BinaryObjectScanner.Wrappers.csproj index 2827f825..c8f0dd20 100644 --- a/BinaryObjectScanner.Wrappers/BinaryObjectScanner.Wrappers.csproj +++ b/BinaryObjectScanner.Wrappers/BinaryObjectScanner.Wrappers.csproj @@ -30,8 +30,8 @@ - - + + diff --git a/BinaryObjectScanner.Wrappers/CFB.cs b/BinaryObjectScanner.Wrappers/CFB.cs deleted file mode 100644 index 698541eb..00000000 --- a/BinaryObjectScanner.Wrappers/CFB.cs +++ /dev/null @@ -1,365 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class CFB : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Compact File Binary"; - - #endregion - - #region Extension Properties - - /// - /// Normal sector size in bytes - /// -#if NET48 - public long SectorSize => (long)Math.Pow(2, this.Model.Header.SectorShift); -#else - public long SectorSize => (long)Math.Pow(2, this.Model.Header?.SectorShift ?? 0); -#endif - - /// - /// Mini sector size in bytes - /// -#if NET48 - public long MiniSectorSize => (long)Math.Pow(2, this.Model.Header.MiniSectorShift); -#else - public long MiniSectorSize => (long)Math.Pow(2, this.Model.Header?.MiniSectorShift ?? 0); -#endif - - #endregion - - #region Constructors - - /// -#if NET48 - public CFB(SabreTools.Models.CFB.Binary model, byte[] data, int offset) -#else - public CFB(SabreTools.Models.CFB.Binary? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public CFB(SabreTools.Models.CFB.Binary model, Stream data) -#else - public CFB(SabreTools.Models.CFB.Binary? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a Compound File Binary from a byte array and offset - /// - /// Byte array representing the archive - /// Offset within the array to parse - /// A Compound File Binary wrapper on success, null on failure -#if NET48 - public static CFB Create(byte[] data, int offset) -#else - public static CFB? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a Compound File Binary from a Stream - /// - /// Stream representing the archive - /// A Compound File Binary wrapper on success, null on failure -#if NET48 - public static CFB Create(Stream data) -#else - public static CFB? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var binary = new SabreTools.Serialization.Streams.CFB().Deserialize(data); - if (binary == null) - return null; - - try - { - return new CFB(binary, data); - } - catch - { - return null; - } - } - - #endregion - - #region FAT Sector Data - - /// - /// Get the ordered FAT sector chain for a given starting sector - /// - /// Initial FAT sector - /// Ordered list of sector numbers, null on error -#if NET48 - public List GetFATSectorChain(SabreTools.Models.CFB.SectorNumber startingSector) -#else - public List? GetFATSectorChain(SabreTools.Models.CFB.SectorNumber? startingSector) -#endif - { - // If we have an invalid sector -#if NET48 - if (startingSector < 0 || this.Model.FATSectorNumbers == null || (long)startingSector >= this.Model.FATSectorNumbers.Length) -#else - if (startingSector == null || startingSector < 0 || this.Model.FATSectorNumbers == null || (long)startingSector >= this.Model.FATSectorNumbers.Length) -#endif - return null; - - // Setup the returned list -#if NET48 - var sectors = new List { startingSector }; -#else - var sectors = new List { startingSector }; -#endif - - var lastSector = startingSector; - while (true) - { -#if NET6_0_OR_GREATER - if (lastSector == null) - break; -#endif - - // Get the next sector from the lookup table -#if NET48 - var nextSector = this.Model.FATSectorNumbers[(uint)lastSector]; -#else - var nextSector = this.Model.FATSectorNumbers[(uint)lastSector!.Value]; -#endif - - // If we have an end of chain or free sector - if (nextSector == SabreTools.Models.CFB.SectorNumber.ENDOFCHAIN || nextSector == SabreTools.Models.CFB.SectorNumber.FREESECT) - break; - - // Add the next sector to the list and replace the last sector - sectors.Add(nextSector); - lastSector = nextSector; - } - - return sectors; - } - - /// - /// Get the data for the FAT sector chain starting at a given starting sector - /// - /// Initial FAT sector - /// Ordered list of sector numbers, null on error -#if NET48 - public byte[] GetFATSectorChainData(SabreTools.Models.CFB.SectorNumber startingSector) -#else - public byte[]? GetFATSectorChainData(SabreTools.Models.CFB.SectorNumber startingSector) -#endif - { - // Get the sector chain first - var sectorChain = GetFATSectorChain(startingSector); - if (sectorChain == null) - return null; - - // Sequentially read the sectors - var data = new List(); - for (int i = 0; i < sectorChain.Count; i++) - { - // Try to get the sector data offset - int sectorDataOffset = (int)FATSectorToFileOffset(sectorChain[i]); - if (sectorDataOffset < 0 || sectorDataOffset >= GetEndOfFile()) - return null; - - // Try to read the sector data - var sectorData = ReadFromDataSource(sectorDataOffset, (int)SectorSize); - if (sectorData == null) - return null; - - // Add the sector data to the output - data.AddRange(sectorData); - } - - return data.ToArray(); - } - - /// - /// Convert a FAT sector value to a byte offset - /// - /// Sector to convert - /// File offset in bytes, -1 on error -#if NET48 - public long FATSectorToFileOffset(SabreTools.Models.CFB.SectorNumber sector) -#else - public long FATSectorToFileOffset(SabreTools.Models.CFB.SectorNumber? sector) -#endif - { - // If we have an invalid sector number -#if NET48 - if (sector > SabreTools.Models.CFB.SectorNumber.MAXREGSECT) -#else - if (sector == null || sector > SabreTools.Models.CFB.SectorNumber.MAXREGSECT) -#endif - return -1; - - // Convert based on the sector shift value - return (long)(sector + 1) * SectorSize; - } - - #endregion - - #region Mini FAT Sector Data - - /// - /// Get the ordered Mini FAT sector chain for a given starting sector - /// - /// Initial Mini FAT sector - /// Ordered list of sector numbers, null on error -#if NET48 - public List GetMiniFATSectorChain(SabreTools.Models.CFB.SectorNumber startingSector) -#else - public List? GetMiniFATSectorChain(SabreTools.Models.CFB.SectorNumber? startingSector) -#endif - { - // If we have an invalid sector -#if NET48 - if (startingSector < 0 || this.Model.MiniFATSectorNumbers == null || (long)startingSector >= this.Model.MiniFATSectorNumbers.Length) -#else - if (startingSector == null || startingSector < 0 || this.Model.MiniFATSectorNumbers == null || (long)startingSector >= this.Model.MiniFATSectorNumbers.Length) -#endif - return null; - - // Setup the returned list -#if NET48 - var sectors = new List { startingSector }; -#else - var sectors = new List { startingSector }; -#endif - - var lastSector = startingSector; - while (true) - { -#if NET6_0_OR_GREATER - if (lastSector == null) - break; -#endif - - // Get the next sector from the lookup table -#if NET48 - var nextSector = this.Model.MiniFATSectorNumbers[(uint)lastSector]; -#else - var nextSector = this.Model.MiniFATSectorNumbers[(uint)lastSector!.Value]; -#endif - - // If we have an end of chain or free sector - if (nextSector == SabreTools.Models.CFB.SectorNumber.ENDOFCHAIN || nextSector == SabreTools.Models.CFB.SectorNumber.FREESECT) - break; - - // Add the next sector to the list and replace the last sector - sectors.Add(nextSector); - lastSector = nextSector; - } - - return sectors; - } - - /// - /// Get the data for the Mini FAT sector chain starting at a given starting sector - /// - /// Initial Mini FAT sector - /// Ordered list of sector numbers, null on error -#if NET48 - public byte[] GetMiniFATSectorChainData(SabreTools.Models.CFB.SectorNumber startingSector) -#else - public byte[]? GetMiniFATSectorChainData(SabreTools.Models.CFB.SectorNumber startingSector) -#endif - { - // Get the sector chain first - var sectorChain = GetMiniFATSectorChain(startingSector); - if (sectorChain == null) - return null; - - // Sequentially read the sectors - var data = new List(); - for (int i = 0; i < sectorChain.Count; i++) - { - // Try to get the sector data offset - int sectorDataOffset = (int)MiniFATSectorToFileOffset(sectorChain[i]); - if (sectorDataOffset < 0 || sectorDataOffset >= GetEndOfFile()) - return null; - - // Try to read the sector data - var sectorData = ReadFromDataSource(sectorDataOffset, (int)MiniSectorSize); - if (sectorData == null) - return null; - - // Add the sector data to the output - data.AddRange(sectorData); - } - - return data.ToArray(); - } - - /// - /// Convert a Mini FAT sector value to a byte offset - /// - /// Sector to convert - /// File offset in bytes, -1 on error -#if NET48 - public long MiniFATSectorToFileOffset(SabreTools.Models.CFB.SectorNumber sector) -#else - public long MiniFATSectorToFileOffset(SabreTools.Models.CFB.SectorNumber? sector) -#endif - { - // If we have an invalid sector number -#if NET48 - if (sector > SabreTools.Models.CFB.SectorNumber.MAXREGSECT) -#else - if (sector == null || sector > SabreTools.Models.CFB.SectorNumber.MAXREGSECT) -#endif - return -1; - - // Convert based on the sector shift value - return (long)(sector + 1) * MiniSectorSize; - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.CFB.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/CIA.cs b/BinaryObjectScanner.Wrappers/CIA.cs deleted file mode 100644 index 5604c818..00000000 --- a/BinaryObjectScanner.Wrappers/CIA.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class CIA : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "CTR Importable Archive (CIA)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public CIA(SabreTools.Models.N3DS.CIA model, byte[] data, int offset) -#else - public CIA(SabreTools.Models.N3DS.CIA? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public CIA(SabreTools.Models.N3DS.CIA model, Stream data) -#else - public CIA(SabreTools.Models.N3DS.CIA? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a CIA archive from a byte array and offset - /// - /// Byte array representing the archive - /// Offset within the array to parse - /// A CIA archive wrapper on success, null on failure -#if NET48 - public static CIA Create(byte[] data, int offset) -#else - public static CIA? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a CIA archive from a Stream - /// - /// Stream representing the archive - /// A CIA archive wrapper on success, null on failure -#if NET48 - public static CIA Create(Stream data) -#else - public static CIA? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var archive = new SabreTools.Serialization.Streams.CIA().Deserialize(data); - if (archive == null) - return null; - - try - { - return new CIA(archive, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.CIA.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/ConcreteInterfaceSerializer.cs b/BinaryObjectScanner.Wrappers/ConcreteInterfaceSerializer.cs deleted file mode 100644 index ed062464..00000000 --- a/BinaryObjectScanner.Wrappers/ConcreteInterfaceSerializer.cs +++ /dev/null @@ -1,82 +0,0 @@ -#if NET6_0_OR_GREATER -using System; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace BinaryObjectScanner.Wrappers -{ - /// - /// Serializer class for abstract classes - /// - /// - internal class ConcreteAbstractSerializer : JsonConverterFactory - { - public override bool CanConvert(Type typeToConvert) => typeToConvert.IsAbstract; - - class ConcreteAbstractSerializerOfType : JsonConverter - { - static ConcreteAbstractSerializerOfType() - { - if (!typeof(TAbstract).IsAbstract && !typeof(TAbstract).IsInterface) - throw new NotImplementedException(string.Format("Concrete class {0} is not supported", typeof(TAbstract))); - } - - public override TAbstract? Read(ref System.Text.Json.Utf8JsonReader reader, Type typeToConvert, System.Text.Json.JsonSerializerOptions options) => - throw new NotImplementedException(); - - public override void Write(System.Text.Json.Utf8JsonWriter writer, TAbstract value, System.Text.Json.JsonSerializerOptions options) => - JsonSerializer.Serialize(writer, value!, options); - } - - public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) => - (JsonConverter)Activator.CreateInstance( - typeof(ConcreteAbstractSerializerOfType<>).MakeGenericType(new Type[] { type }), - BindingFlags.Instance | BindingFlags.Public, - binder: null, - args: Array.Empty(), - culture: null).ThrowOnNull(); - } - - /// - /// Serializer class for interfaces - /// - /// - internal class ConcreteInterfaceSerializer : JsonConverterFactory - { - public override bool CanConvert(Type typeToConvert) => typeToConvert.IsInterface; - - class ConcreteInterfaceSerializerOfType : JsonConverter - { - static ConcreteInterfaceSerializerOfType() - { - if (!typeof(TInterface).IsAbstract && !typeof(TInterface).IsInterface) - throw new NotImplementedException(string.Format("Concrete class {0} is not supported", typeof(TInterface))); - } - - public override TInterface? Read(ref System.Text.Json.Utf8JsonReader reader, Type typeToConvert, System.Text.Json.JsonSerializerOptions options) => - throw new NotImplementedException(); - - public override void Write(System.Text.Json.Utf8JsonWriter writer, TInterface value, System.Text.Json.JsonSerializerOptions options) => - JsonSerializer.Serialize(writer, value!, options); - } - - public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) => - (JsonConverter)Activator.CreateInstance( - typeof(ConcreteInterfaceSerializerOfType<>).MakeGenericType(new Type[] { type }), - BindingFlags.Instance | BindingFlags.Public, - binder: null, - args: Array.Empty(), - culture: null).ThrowOnNull(); - } - - /// - /// Extensions for generic object types - /// - /// - internal static class ObjectExtensions - { - public static T ThrowOnNull(this T? value) where T : class => value ?? throw new ArgumentNullException(); - } -} -#endif \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/Enums.cs b/BinaryObjectScanner.Wrappers/Enums.cs deleted file mode 100644 index 401db1bd..00000000 --- a/BinaryObjectScanner.Wrappers/Enums.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace BinaryObjectScanner.Wrappers -{ - /// - /// Location that the data originated from - /// - public enum DataSource - { - /// - /// Unknown origin / testing - /// - UNKNOWN = 0, - - /// - /// Byte array with offset - /// - ByteArray = 1, - - /// - /// Stream - /// - Stream = 2, - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/Extraction.cs b/BinaryObjectScanner.Wrappers/Extraction.cs new file mode 100644 index 00000000..2aba7080 --- /dev/null +++ b/BinaryObjectScanner.Wrappers/Extraction.cs @@ -0,0 +1,1682 @@ +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/BinaryObjectScanner.Wrappers/GCF.cs b/BinaryObjectScanner.Wrappers/GCF.cs deleted file mode 100644 index d4caaa95..00000000 --- a/BinaryObjectScanner.Wrappers/GCF.cs +++ /dev/null @@ -1,416 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class GCF : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Half-Life Game Cache File (GCF)"; - - #endregion - - #region Extension Properties - - /// - /// Set of all files and their information - /// -#if NET48 - public FileInfo[] Files -#else - public FileInfo[]? Files -#endif - { - get - { - // Use the cached value if we have it - if (_files != null) - return _files; - - // If we don't have a required property - if (this.Model.DirectoryEntries == null || this.Model.DirectoryMapEntries == null || this.Model.BlockEntries == null) - return null; - - // Otherwise, scan and build the files - var files = new List(); - for (int i = 0; i < this.Model.DirectoryEntries.Length; i++) - { - // Get the directory entry - var directoryEntry = this.Model.DirectoryEntries[i]; - var directoryMapEntry = this.Model.DirectoryMapEntries[i]; - if (directoryEntry == null || directoryMapEntry == null) - continue; - - // If we have a directory, skip for now - if (!directoryEntry.DirectoryFlags.HasFlag(SabreTools.Models.GCF.HL_GCF_FLAG.HL_GCF_FLAG_FILE)) - continue; - - // Otherwise, start building the file info - var fileInfo = new FileInfo() - { - Size = directoryEntry.ItemSize, - Encrypted = directoryEntry.DirectoryFlags.HasFlag(SabreTools.Models.GCF.HL_GCF_FLAG.HL_GCF_FLAG_ENCRYPTED), - }; - var pathParts = new List { directoryEntry.Name ?? string.Empty }; -#if NET48 - var blockEntries = new List(); -#else - var blockEntries = new List(); -#endif - - // Traverse the parent tree - uint index = directoryEntry.ParentIndex; - while (index != 0xFFFFFFFF) - { - var parentDirectoryEntry = this.Model.DirectoryEntries[index]; - if (parentDirectoryEntry == null) - break; - - pathParts.Add(parentDirectoryEntry.Name ?? string.Empty); - index = parentDirectoryEntry.ParentIndex; - } - - // Traverse the block entries - index = directoryMapEntry.FirstBlockIndex; - while (index != this.Model.DataBlockHeader?.BlockCount) - { - var nextBlock = this.Model.BlockEntries[index]; - if (nextBlock == null) - break; - - blockEntries.Add(nextBlock); - index = nextBlock.NextBlockEntryIndex; - } - - // Reverse the path parts because of traversal - pathParts.Reverse(); - - // Build the remaining file info - fileInfo.Path = Path.Combine(pathParts.ToArray()); - fileInfo.BlockEntries = blockEntries.ToArray(); - - // Add the file info and continue - files.Add(fileInfo); - } - - // Set and return the file infos - _files = files.ToArray(); - return _files; - } - } - - /// - /// Set of all data block offsets - /// -#if NET48 - public long[] DataBlockOffsets -#else - public long[]? DataBlockOffsets -#endif - { - get - { - // Use the cached value if we have it - if (_dataBlockOffsets != null) - return _dataBlockOffsets; - -#if NET6_0_OR_GREATER - // If we don't have a block count, offset, or size - if (this.Model.DataBlockHeader?.BlockCount == null || this.Model.DataBlockHeader?.FirstBlockOffset == null || this.Model.DataBlockHeader?.BlockSize == null) - return null; -#endif - - // Otherwise, build the data block set - _dataBlockOffsets = new long[this.Model.DataBlockHeader.BlockCount]; - for (int i = 0; i < this.Model.DataBlockHeader.BlockCount; i++) - { - long dataBlockOffset = this.Model.DataBlockHeader.FirstBlockOffset + (i * this.Model.DataBlockHeader.BlockSize); - _dataBlockOffsets[i] = dataBlockOffset; - } - - // Return the set of data blocks - return _dataBlockOffsets; - } - } - - #endregion - - #region Instance Variables - - /// - /// Set of all files and their information - /// -#if NET48 - private FileInfo[] _files = null; -#else - private FileInfo[]? _files = null; -#endif - - /// - /// Set of all data block offsets - /// -#if NET48 - private long[] _dataBlockOffsets = null; -#else - private long[]? _dataBlockOffsets = null; -#endif - - #endregion - - #region Constructors - - /// -#if NET48 - public GCF(SabreTools.Models.GCF.File model, byte[] data, int offset) -#else - public GCF(SabreTools.Models.GCF.File? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public GCF(SabreTools.Models.GCF.File model, Stream data) -#else - public GCF(SabreTools.Models.GCF.File? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create an GCF from a byte array and offset - /// - /// Byte array representing the GCF - /// Offset within the array to parse - /// An GCF wrapper on success, null on failure -#if NET48 - public static GCF Create(byte[] data, int offset) -#else - public static GCF? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a GCF from a Stream - /// - /// Stream representing the GCF - /// An GCF wrapper on success, null on failure -#if NET48 - public static GCF Create(Stream data) -#else - public static GCF? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var file = new SabreTools.Serialization.Streams.GCF().Deserialize(data); - if (file == null) - return null; - - try - { - return new GCF(file, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.GCF.Print(builder, this.Model); - return builder; - } - - #endregion - - #region Extraction - - /// - /// Extract all files from the GCF to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public bool ExtractAll(string outputDirectory) - { - // If we have no files - if (Files == null || Files.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < Files.Length; i++) - { - allExtracted &= 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 bool ExtractFile(int index, string outputDirectory) - { - // If we have no files - if (Files == null || Files.Length == 0 || DataBlockOffsets == null) - return false; - - // If the files index is invalid - if (index < 0 || index >= Files.Length) - return false; - - // Get the file - var file = 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 = DataBlockOffsets[dataBlockIndex++]; - dataBlockOffsets.Add(dataBlockOffset); - blockEntrySize -= this.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(this.Model.DataBlockHeader?.BlockSize ?? 0, fileSize); -#if NET48 - byte[] data = ReadFromDataSource((int)dataBlockOffsets[i], readSize); -#else - byte[]? data = ReadFromDataSource((int)dataBlockOffsets[i], readSize); -#endif - if (data == null) - return false; - - fs.Write(data, 0, data.Length); - } - } - } - catch - { - return false; - } - - return true; - } - - #endregion - - #region Helper Classes - - /// - /// Class to contain all necessary file information - /// - public sealed class FileInfo - { - /// - /// Full item path - /// -#if NET48 - public string Path; -#else - public string? Path; -#endif - - /// - /// File size - /// - public uint Size; - - /// - /// Indicates if the block is encrypted - /// - public bool Encrypted; - - /// - /// Array of block entries - /// -#if NET48 - public SabreTools.Models.GCF.BlockEntry[] BlockEntries; -#else - public SabreTools.Models.GCF.BlockEntry?[]? BlockEntries; -#endif - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/IWrapper.cs b/BinaryObjectScanner.Wrappers/IWrapper.cs deleted file mode 100644 index 9687819e..00000000 --- a/BinaryObjectScanner.Wrappers/IWrapper.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public interface IWrapper - { - /// - /// Get a human-readable description of the wrapper - /// - string Description(); - - /// - /// Export the item information as pretty-printed text - /// - StringBuilder PrettyPrint(); - -#if NET6_0_OR_GREATER - /// - /// Export the item information as JSON - /// - string ExportJSON(); -#endif - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/InstallShieldCabinet.cs b/BinaryObjectScanner.Wrappers/InstallShieldCabinet.cs deleted file mode 100644 index 30b3816e..00000000 --- a/BinaryObjectScanner.Wrappers/InstallShieldCabinet.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public partial class InstallShieldCabinet : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "InstallShield Cabinet"; - - #endregion - - #region Extension Properties - - /// - /// The major version of the cabinet - /// - public int MajorVersion - { - get - { - uint majorVersion = this.Model.CommonHeader?.Version ?? 0; - if (majorVersion >> 24 == 1) - { - majorVersion = (majorVersion >> 12) & 0x0F; - } - else if (majorVersion >> 24 == 2 || majorVersion >> 24 == 4) - { - majorVersion = majorVersion & 0xFFFF; - if (majorVersion != 0) - majorVersion /= 100; - } - - return (int)majorVersion; - } - } - - #endregion - - #region Constructors - - /// -#if NET48 - public InstallShieldCabinet(SabreTools.Models.InstallShieldCabinet.Cabinet model, byte[] data, int offset) -#else - public InstallShieldCabinet(SabreTools.Models.InstallShieldCabinet.Cabinet? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public InstallShieldCabinet(SabreTools.Models.InstallShieldCabinet.Cabinet model, Stream data) -#else - public InstallShieldCabinet(SabreTools.Models.InstallShieldCabinet.Cabinet? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create an InstallShield Cabinet from a byte array and offset - /// - /// Byte array representing the cabinet - /// Offset within the array to parse - /// A cabinet wrapper on success, null on failure -#if NET48 - public static InstallShieldCabinet Create(byte[] data, int offset) -#else - public static InstallShieldCabinet? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a InstallShield Cabinet from a Stream - /// - /// Stream representing the cabinet - /// A cabinet wrapper on success, null on failure -#if NET48 - public static InstallShieldCabinet Create(Stream data) -#else - public static InstallShieldCabinet? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var cabinet = new SabreTools.Serialization.Streams.InstallShieldCabinet().Deserialize(data); - if (cabinet == null) - return null; - - try - { - return new InstallShieldCabinet(cabinet, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.InstallShieldCabinet.Print(builder, this.Model); - return builder; - } - - #endregion - } -} diff --git a/BinaryObjectScanner.Wrappers/LinearExecutable.cs b/BinaryObjectScanner.Wrappers/LinearExecutable.cs deleted file mode 100644 index 7c429591..00000000 --- a/BinaryObjectScanner.Wrappers/LinearExecutable.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class LinearExecutable : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Linear Executable (LE/LX)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public LinearExecutable(SabreTools.Models.LinearExecutable.Executable model, byte[] data, int offset) -#else - public LinearExecutable(SabreTools.Models.LinearExecutable.Executable? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public LinearExecutable(SabreTools.Models.LinearExecutable.Executable model, Stream data) -#else - public LinearExecutable(SabreTools.Models.LinearExecutable.Executable? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - }/// - /// Create an LE/LX executable from a byte array and offset - /// - /// Byte array representing the executable - /// Offset within the array to parse - /// An LE/LX executable wrapper on success, null on failure -#if NET48 - public static LinearExecutable Create(byte[] data, int offset) -#else - public static LinearExecutable? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create an LE/LX executable from a Stream - /// - /// Stream representing the executable - /// An LE/LX executable wrapper on success, null on failure -#if NET48 - public static LinearExecutable Create(Stream data) -#else - public static LinearExecutable? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var executable = new SabreTools.Serialization.Streams.LinearExecutable().Deserialize(data); - if (executable == null) - return null; - - try - { - return new LinearExecutable(executable, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.LinearExecutable.Print(builder, this.Model); - return builder; - } - - #endregion - - #region REMOVE -- DO NOT USE - - /// - /// Read an arbitrary range from the source - /// - /// The start of where to read data from, -1 means start of source - /// How many bytes to read, -1 means read until end - /// Byte array representing the range, null on error - [Obsolete] -#if NET48 - public byte[] ReadArbitraryRange(int rangeStart = -1, int length = -1) -#else - public byte[]? ReadArbitraryRange(int rangeStart = -1, int length = -1) -#endif - { - // If we have an unset range start, read from the start of the source - if (rangeStart == -1) - rangeStart = 0; - - // If we have an unset length, read the whole source - if (length == -1) - { - switch (_dataSource) - { - case DataSource.ByteArray: -#if NET48 - length = _byteArrayData.Length - _byteArrayOffset; -#else - length = _byteArrayData!.Length - _byteArrayOffset; -#endif - break; - - case DataSource.Stream: -#if NET48 - length = (int)_streamData.Length; -#else - length = (int)_streamData!.Length; -#endif - break; - } - } - - return ReadFromDataSource(rangeStart, length); - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/MSDOS.cs b/BinaryObjectScanner.Wrappers/MSDOS.cs deleted file mode 100644 index 6a063a5c..00000000 --- a/BinaryObjectScanner.Wrappers/MSDOS.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class MSDOS : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "MS-DOS Executable"; - - #endregion - - #region Constructors - - /// -#if NET48 - public MSDOS(SabreTools.Models.MSDOS.Executable model, byte[] data, int offset) -#else - public MSDOS(SabreTools.Models.MSDOS.Executable? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public MSDOS(SabreTools.Models.MSDOS.Executable model, Stream data) -#else - public MSDOS(SabreTools.Models.MSDOS.Executable? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - }/// - /// Create an MS-DOS executable from a byte array and offset - /// - /// Byte array representing the executable - /// Offset within the array to parse - /// An MS-DOS executable wrapper on success, null on failure -#if NET48 - public static MSDOS Create(byte[] data, int offset) -#else - public static MSDOS? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create an MS-DOS executable from a Stream - /// - /// Stream representing the executable - /// An MS-DOS executable wrapper on success, null on failure -#if NET48 - public static MSDOS Create(Stream data) -#else - public static MSDOS? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var executable = new SabreTools.Serialization.Streams.MSDOS().Deserialize(data); - if (executable == null) - return null; - - try - { - return new MSDOS(executable, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.MSDOS.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/MicrosoftCabinet.cs b/BinaryObjectScanner.Wrappers/MicrosoftCabinet.cs deleted file mode 100644 index 5e92db3a..00000000 --- a/BinaryObjectScanner.Wrappers/MicrosoftCabinet.cs +++ /dev/null @@ -1,372 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public partial class MicrosoftCabinet : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Microsoft Cabinet"; - - #endregion - - #region Constructors - - /// -#if NET48 - public MicrosoftCabinet(SabreTools.Models.MicrosoftCabinet.Cabinet model, byte[] data, int offset) -#else - public MicrosoftCabinet(SabreTools.Models.MicrosoftCabinet.Cabinet? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public MicrosoftCabinet(SabreTools.Models.MicrosoftCabinet.Cabinet model, Stream data) -#else - public MicrosoftCabinet(SabreTools.Models.MicrosoftCabinet.Cabinet? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - }/// - /// Create a Microsoft Cabinet from a byte array and offset - /// - /// Byte array representing the cabinet - /// Offset within the array to parse - /// A cabinet wrapper on success, null on failure -#if NET48 - public static MicrosoftCabinet Create(byte[] data, int offset) -#else - public static MicrosoftCabinet? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a Microsoft Cabinet from a Stream - /// - /// Stream representing the cabinet - /// A cabinet wrapper on success, null on failure -#if NET48 - public static MicrosoftCabinet Create(Stream data) -#else - public static MicrosoftCabinet? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var cabinet = new SabreTools.Serialization.Streams.MicrosoftCabinet().Deserialize(data); - if (cabinet == null) - return null; - - try - { - return new MicrosoftCabinet(cabinet, data); - } - catch - { - return null; - } - } - - #endregion - - #region Checksumming - - /// - /// The computation and verification of checksums found in CFDATA structure entries cabinet files is - /// done by using a function described by the following mathematical notation. When checksums are - /// not supplied by the cabinet file creating application, the checksum field is set to 0 (zero). Cabinet - /// extracting applications do not compute or verify the checksum if the field is set to 0 (zero). - /// - private static uint ChecksumData(byte[] data) - { - uint[] C = new uint[4] - { - S(data, 1, data.Length), - S(data, 2, data.Length), - S(data, 3, data.Length), - S(data, 4, data.Length), - }; - - return C[0] ^ C[1] ^ C[2] ^ C[3]; - } - - /// - /// Individual algorithmic step - /// - private static uint S(byte[] a, int b, int x) - { - int n = a.Length; - - if (x < 4 && b > n % 4) - return 0; - else if (x < 4 && b <= n % 4) - return a[n - b + 1]; - else // if (x >= 4) - return a[n - x + b] ^ S(a, b, x - 4); - } - - #endregion - - #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 byte[] GetUncompressedData(int folderIndex) -#else - public byte[]? GetUncompressedData(int folderIndex) -#endif - { - // If we have an invalid folder index - if (folderIndex < 0 || this.Model.Folders == null || folderIndex >= this.Model.Folders.Length) - return null; - - // Get the folder header - var folder = this.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 bool ExtractAll(string outputDirectory) - { - // If we have no files - if (this.Model.Files == null || this.Model.Files.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.Files.Length; i++) - { - allExtracted &= 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 bool ExtractFile(int index, string outputDirectory) - { - // If we have an invalid file index - if (index < 0 || this.Model.Files == null || index >= this.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 = this.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(index); -#else - byte[]? fileData = 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 DateTime for a particular file index - /// - /// File index to check - /// DateTime representing the file time, null on error - public DateTime? GetDateTime(int fileIndex) - { - // If we have an invalid file index - if (fileIndex < 0 || this.Model.Files == null || fileIndex >= this.Model.Files.Length) - return null; - - // Get the file header - var file = this.Model.Files[fileIndex]; - if (file == null) - return null; - - // If we have an invalid DateTime - if (file.Date == 0 && file.Time == 0) - return null; - - try - { - // Date property - int year = (file.Date >> 9) + 1980; - int month = (file.Date >> 5) & 0x0F; - int day = file.Date & 0x1F; - - // Time property - int hour = file.Time >> 11; - int minute = (file.Time >> 5) & 0x3F; - int second = (file.Time << 1) & 0x3E; - - return new DateTime(year, month, day, hour, minute, second); - } - catch - { - return DateTime.MinValue; - } - } - - /// - /// Get the uncompressed data associated with a file - /// - /// File index to check - /// Byte array representing the data, null on error -#if NET48 - public byte[] GetFileData(int fileIndex) -#else - public byte[]? GetFileData(int fileIndex) -#endif - { - // If we have an invalid file index - if (fileIndex < 0 || this.Model.Files == null || fileIndex >= this.Model.Files.Length) - return null; - - // Get the file header - var file = this.Model.Files[fileIndex]; - if (file == null || file.FileSize == 0) - return null; - - // Get the parent folder data -#if NET48 - byte[] folderData = GetUncompressedData((int)file.FolderIndex); -#else - byte[]? folderData = 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 - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.MicrosoftCabinet.Print(builder, this.Model); - return builder; - } - - #endregion - } -} diff --git a/BinaryObjectScanner.Wrappers/N3DS.cs b/BinaryObjectScanner.Wrappers/N3DS.cs deleted file mode 100644 index dfbb6c23..00000000 --- a/BinaryObjectScanner.Wrappers/N3DS.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class N3DS : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Nintendo 3DS Cart Image"; - - #endregion - - #region Constructors - - /// -#if NET48 - public N3DS(SabreTools.Models.N3DS.Cart model, byte[] data, int offset) -#else - public N3DS(SabreTools.Models.N3DS.Cart? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public N3DS(SabreTools.Models.N3DS.Cart model, Stream data) -#else - public N3DS(SabreTools.Models.N3DS.Cart? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a 3DS cart image from a byte array and offset - /// - /// Byte array representing the archive - /// Offset within the array to parse - /// A 3DS cart image wrapper on success, null on failure -#if NET48 - public static N3DS Create(byte[] data, int offset) -#else - public static N3DS? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a 3DS cart image from a Stream - /// - /// Stream representing the archive - /// A 3DS cart image wrapper on success, null on failure -#if NET48 - public static N3DS Create(Stream data) -#else - public static N3DS? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var archive = new SabreTools.Serialization.Streams.N3DS().Deserialize(data); - if (archive == null) - return null; - - try - { - return new N3DS(archive, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.N3DS.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/NCF.cs b/BinaryObjectScanner.Wrappers/NCF.cs deleted file mode 100644 index 36a66220..00000000 --- a/BinaryObjectScanner.Wrappers/NCF.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class NCF : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Half-Life No Cache File (NCF)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public NCF(SabreTools.Models.NCF.File model, byte[] data, int offset) -#else - public NCF(SabreTools.Models.NCF.File? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public NCF(SabreTools.Models.NCF.File model, Stream data) -#else - public NCF(SabreTools.Models.NCF.File? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create an NCF from a byte array and offset - /// - /// Byte array representing the NCF - /// Offset within the array to parse - /// An NCF wrapper on success, null on failure -#if NET48 - public static NCF Create(byte[] data, int offset) -#else - public static NCF? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a NCF from a Stream - /// - /// Stream representing the NCF - /// An NCF wrapper on success, null on failure -#if NET48 - public static NCF Create(Stream data) -#else - public static NCF? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var file = new SabreTools.Serialization.Streams.NCF().Deserialize(data); - if (file == null) - return null; - - try - { - return new NCF(file, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.NCF.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/NewExecutable.cs b/BinaryObjectScanner.Wrappers/NewExecutable.cs deleted file mode 100644 index 429bb456..00000000 --- a/BinaryObjectScanner.Wrappers/NewExecutable.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class NewExecutable : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "New Executable (NE)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public NewExecutable(SabreTools.Models.NewExecutable.Executable model, byte[] data, int offset) -#else - public NewExecutable(SabreTools.Models.NewExecutable.Executable? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public NewExecutable(SabreTools.Models.NewExecutable.Executable model, Stream data) -#else - public NewExecutable(SabreTools.Models.NewExecutable.Executable? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create an NE executable from a byte array and offset - /// - /// Byte array representing the executable - /// Offset within the array to parse - /// An NE executable wrapper on success, null on failure -#if NET48 - public static NewExecutable Create(byte[] data, int offset) -#else - public static NewExecutable? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create an NE executable from a Stream - /// - /// Stream representing the executable - /// An NE executable wrapper on success, null on failure -#if NET48 - public static NewExecutable Create(Stream data) -#else - public static NewExecutable? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var executable = new SabreTools.Serialization.Streams.NewExecutable().Deserialize(data); - if (executable == null) - return null; - - try - { - return new NewExecutable(executable, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.NewExecutable.Print(builder, this.Model); - return builder; - } - - #endregion - - #region REMOVE -- DO NOT USE - - /// - /// Read an arbitrary range from the source - /// - /// The start of where to read data from, -1 means start of source - /// How many bytes to read, -1 means read until end - /// Byte array representing the range, null on error - [Obsolete] -#if NET48 - public byte[] ReadArbitraryRange(int rangeStart = -1, int length = -1) -#else - public byte[]? ReadArbitraryRange(int rangeStart = -1, int length = -1) -#endif - { - // If we have an unset range start, read from the start of the source - if (rangeStart == -1) - rangeStart = 0; - - // If we have an unset length, read the whole source - if (length == -1) - { - switch (_dataSource) - { - case DataSource.ByteArray: -#if NET48 - length = _byteArrayData.Length - _byteArrayOffset; -#else - length = _byteArrayData!.Length - _byteArrayOffset; -#endif - break; - - case DataSource.Stream: -#if NET48 - length = (int)_streamData.Length; -#else - length = (int)_streamData!.Length; -#endif - break; - } - } - - return ReadFromDataSource(rangeStart, length); - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/Nitro.cs b/BinaryObjectScanner.Wrappers/Nitro.cs deleted file mode 100644 index 1929d312..00000000 --- a/BinaryObjectScanner.Wrappers/Nitro.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class Nitro : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Nintendo DS/DSi Cart Image"; - - #endregion - - #region Constructors - - /// -#if NET48 - public Nitro(SabreTools.Models.Nitro.Cart model, byte[] data, int offset) -#else - public Nitro(SabreTools.Models.Nitro.Cart? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public Nitro(SabreTools.Models.Nitro.Cart model, Stream data) -#else - public Nitro(SabreTools.Models.Nitro.Cart? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a NDS cart image from a byte array and offset - /// - /// Byte array representing the archive - /// Offset within the array to parse - /// A NDS cart image wrapper on success, null on failure -#if NET48 - public static Nitro Create(byte[] data, int offset) -#else - public static Nitro? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a NDS cart image from a Stream - /// - /// Stream representing the archive - /// A NDS cart image wrapper on success, null on failure -#if NET48 - public static Nitro Create(Stream data) -#else - public static Nitro? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var archive = new SabreTools.Serialization.Streams.Nitro().Deserialize(data); - if (archive == null) - return null; - - try - { - return new Nitro(archive, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.Nitro.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/PAK.cs b/BinaryObjectScanner.Wrappers/PAK.cs deleted file mode 100644 index d07eb6d2..00000000 --- a/BinaryObjectScanner.Wrappers/PAK.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class PAK : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Half-Life Package File (PAK)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public PAK(SabreTools.Models.PAK.File model, byte[] data, int offset) -#else - public PAK(SabreTools.Models.PAK.File? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public PAK(SabreTools.Models.PAK.File model, Stream data) -#else - public PAK(SabreTools.Models.PAK.File? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a PAK from a byte array and offset - /// - /// Byte array representing the PAK - /// Offset within the array to parse - /// A PAK wrapper on success, null on failure -#if NET48 - public static PAK Create(byte[] data, int offset) -#else - public static PAK? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a PAK from a Stream - /// - /// Stream representing the PAK - /// A PAK wrapper on success, null on failure -#if NET48 - public static PAK Create(Stream data) -#else - public static PAK? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var file = new SabreTools.Serialization.Streams.PAK().Deserialize(data); - if (file == null) - return null; - - try - { - return new PAK(file, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.PAK.Print(builder, this.Model); - return builder; - } - - #endregion - - #region Extraction - - /// - /// Extract all files from the PAK to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public bool ExtractAll(string outputDirectory) - { - // If we have no directory items - if (this.Model.DirectoryItems == null || this.Model.DirectoryItems.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.DirectoryItems.Length; i++) - { - allExtracted &= 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 bool ExtractFile(int index, string outputDirectory) - { - // If we have no directory items - if (this.Model.DirectoryItems == null || this.Model.DirectoryItems.Length == 0) - return false; - - // If the directory item index is invalid - if (index < 0 || index >= this.Model.DirectoryItems.Length) - return false; - - // Get the directory item - var directoryItem = this.Model.DirectoryItems[index]; - if (directoryItem == null) - return false; - - // Read the item data -#if NET48 - byte[] data = ReadFromDataSource((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength); -#else - byte[]? data = 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 - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/PFF.cs b/BinaryObjectScanner.Wrappers/PFF.cs deleted file mode 100644 index d9161276..00000000 --- a/BinaryObjectScanner.Wrappers/PFF.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class PFF : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "NovaLogic Game Archive Format (PFF)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public PFF(SabreTools.Models.PFF.Archive model, byte[] data, int offset) -#else - public PFF(SabreTools.Models.PFF.Archive? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public PFF(SabreTools.Models.PFF.Archive model, Stream data) -#else - public PFF(SabreTools.Models.PFF.Archive? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - }/// - /// Create a PFF archive from a byte array and offset - /// - /// Byte array representing the archive - /// Offset within the array to parse - /// A PFF archive wrapper on success, null on failure -#if NET48 - public static PFF Create(byte[] data, int offset) -#else - public static PFF? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a PFF archive from a Stream - /// - /// Stream representing the archive - /// A PFF archive wrapper on success, null on failure -#if NET48 - public static PFF Create(Stream data) -#else - public static PFF? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var archive = new SabreTools.Serialization.Streams.PFF().Deserialize(data); - if (archive == null) - return null; - - try - { - return new PFF(archive, data); - } - catch - { - return null; - } - } - - #endregion - - #region Data - - /// - /// Extract all segments from the PFF to an output directory - /// - /// Output directory to write to - /// True if all segments extracted, false otherwise - public bool ExtractAll(string outputDirectory) - { - // If we have no segments - if (this.Model.Segments == null || this.Model.Segments.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.Segments.Length; i++) - { - allExtracted &= 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 bool ExtractSegment(int index, string outputDirectory) - { - // If we have no segments - if (this.Model.Header?.NumberOfFiles == null || this.Model.Header.NumberOfFiles == 0 || this.Model.Segments == null || this.Model.Segments.Length == 0) - return false; - - // If we have an invalid index - if (index < 0 || index >= this.Model.Segments.Length) - return false; - - // Get the segment information - var file = this.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 = ReadFromDataSource(offset, size); -#else - byte[]? data = 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 Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.PFF.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/PlayJAudioFile.cs b/BinaryObjectScanner.Wrappers/PlayJAudioFile.cs deleted file mode 100644 index becac0da..00000000 --- a/BinaryObjectScanner.Wrappers/PlayJAudioFile.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class PlayJAudioFile : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "PlayJ Audio File (PLJ)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public PlayJAudioFile(SabreTools.Models.PlayJ.AudioFile model, byte[] data, int offset) -#else - public PlayJAudioFile(SabreTools.Models.PlayJ.AudioFile? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public PlayJAudioFile(SabreTools.Models.PlayJ.AudioFile model, Stream data) -#else - public PlayJAudioFile(SabreTools.Models.PlayJ.AudioFile? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a PlayJ audio file from a byte array and offset - /// - /// Byte array representing the archive - /// Offset within the array to parse - /// A PlayJ audio file wrapper on success, null on failure -#if NET48 - public static PlayJAudioFile Create(byte[] data, int offset) -#else - public static PlayJAudioFile? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a PlayJ audio file from a Stream - /// - /// Stream representing the archive - /// A PlayJ audio file wrapper on success, null on failure -#if NET48 - public static PlayJAudioFile Create(Stream data) -#else - public static PlayJAudioFile? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var audioFile = new SabreTools.Serialization.Streams.PlayJAudio().Deserialize(data); - if (audioFile == null) - return null; - - try - { - return new PlayJAudioFile(audioFile, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.PlayJAudioFile.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/PortableExecutable.cs b/BinaryObjectScanner.Wrappers/PortableExecutable.cs deleted file mode 100644 index 3ccabd6e..00000000 --- a/BinaryObjectScanner.Wrappers/PortableExecutable.cs +++ /dev/null @@ -1,2177 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using SabreTools.IO; -using static SabreTools.Serialization.Extensions; - -namespace BinaryObjectScanner.Wrappers -{ - public class PortableExecutable : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Portable Executable (PE)"; - - #endregion - - #region Extension Properties - - /// - /// Header padding data, if it exists - /// -#if NET48 - public byte[] HeaderPaddingData -#else - public byte[]? HeaderPaddingData -#endif - { - get - { - lock (_sourceDataLock) - { - // If we already have cached data, just use that immediately - if (_headerPaddingData != null) - return _headerPaddingData; - - // TODO: Don't scan the known header data as well - - // If the section table is missing - if (this.Model.SectionTable == null) - return null; - - // Populate the raw header padding data based on the source - uint headerStartAddress = this.Model.Stub?.Header?.NewExeHeaderAddr ?? 0; - uint firstSectionAddress = this.Model.SectionTable - .Select(s => s?.PointerToRawData ?? 0) - .Where(s => s != 0) - .OrderBy(s => s) - .First(); - int headerLength = (int)(firstSectionAddress - headerStartAddress); - _headerPaddingData = ReadFromDataSource((int)headerStartAddress, headerLength); - - // Cache and return the header padding data, even if null - return _headerPaddingData; - } - } - } - - /// - /// Header padding strings, if they exist - /// -#if NET48 - public List HeaderPaddingStrings -#else - public List? HeaderPaddingStrings -#endif - { - get - { - lock (_sourceDataLock) - { - // If we already have cached data, just use that immediately - if (_headerPaddingStrings != null) - return _headerPaddingStrings; - - // TODO: Don't scan the known header data as well - - // If the section table is missing - if (this.Model.SectionTable == null) - return null; - - // Populate the raw header padding data based on the source - uint headerStartAddress = this.Model.Stub?.Header?.NewExeHeaderAddr ?? 0; - uint firstSectionAddress = this.Model.SectionTable - .Select(s => s?.PointerToRawData ?? 0) - .Where(s => s != 0) - .OrderBy(s => s) - .First(); - int headerLength = (int)(firstSectionAddress - headerStartAddress); - _headerPaddingStrings = ReadStringsFromDataSource((int)headerStartAddress, headerLength, charLimit: 3); - - // Cache and return the header padding data, even if null - return _headerPaddingStrings; - } - } - } - - /// - /// Entry point data, if it exists - /// -#if NET48 - public byte[] EntryPointData -#else - public byte[]? EntryPointData -#endif - { - get - { - lock (_sourceDataLock) - { - // If the section table is missing - if (this.Model.SectionTable == null) - return null; - - // If the address is missing - if (this.Model.OptionalHeader?.AddressOfEntryPoint == null) - return null; - - // If we have no entry point - int entryPointAddress = (int)this.Model.OptionalHeader.AddressOfEntryPoint.ConvertVirtualAddress(this.Model.SectionTable); - if (entryPointAddress == 0) - return null; - - // If the entry point matches with the start of a section, use that - int entryPointSection = FindEntryPointSectionIndex(); - if (entryPointSection >= 0 && this.Model.OptionalHeader.AddressOfEntryPoint == this.Model.SectionTable[entryPointSection]?.VirtualAddress) - return GetSectionData(entryPointSection); - - // If we already have cached data, just use that immediately - if (_entryPointData != null) - return _entryPointData; - - // Read the first 128 bytes of the entry point - _entryPointData = ReadFromDataSource(entryPointAddress, length: 128); - - // Cache and return the entry point padding data, even if null - return _entryPointData; - } - } - } - - /// - /// Address of the overlay, if it exists - /// - /// - public int OverlayAddress - { - get - { - lock (_sourceDataLock) - { - // Use the cached data if possible - if (_overlayAddress != null) - return _overlayAddress.Value; - - // Get the end of the file, if possible - int endOfFile = GetEndOfFile(); - if (endOfFile == -1) - return -1; - - // If the section table is missing - if (this.Model.SectionTable == null) - return -1; - - // If we have certificate data, use that as the end - if (this.Model.OptionalHeader?.CertificateTable != null) - { - int certificateTableAddress = (int)this.Model.OptionalHeader.CertificateTable.VirtualAddress.ConvertVirtualAddress(this.Model.SectionTable); - if (certificateTableAddress != 0 && certificateTableAddress < endOfFile) - endOfFile = certificateTableAddress; - } - - // Search through all sections and find the furthest a section goes - int endOfSectionData = -1; - foreach (var section in this.Model.SectionTable) - { - // If we have an invalid section - if (section == null) - continue; - - // If we have an invalid section address - int sectionAddress = (int)section.VirtualAddress.ConvertVirtualAddress(this.Model.SectionTable); - if (sectionAddress == 0) - continue; - - // If we have an invalid section size - if (section.SizeOfRawData == 0 && section.VirtualSize == 0) - continue; - - // Get the real section size - int sectionSize; - if (section.SizeOfRawData < section.VirtualSize) - sectionSize = (int)section.VirtualSize; - else - sectionSize = (int)section.SizeOfRawData; - - // Compare and set the end of section data - if (sectionAddress + sectionSize > endOfSectionData) - endOfSectionData = sectionAddress + sectionSize; - } - - // If we didn't find the end of section data - if (endOfSectionData <= 0) - endOfSectionData = -1; - - // Cache and return the position - _overlayAddress = endOfSectionData; - return _overlayAddress.Value; - } - } - } - - /// - /// Overlay data, if it exists - /// - /// -#if NET48 - public byte[] OverlayData -#else - public byte[]? OverlayData -#endif - { - get - { - lock (_sourceDataLock) - { - // Use the cached data if possible - if (_overlayData != null) - return _overlayData; - - // Get the end of the file, if possible - int endOfFile = GetEndOfFile(); - if (endOfFile == -1) - return null; - - // If the section table is missing - if (this.Model.SectionTable == null) - return null; - - // If we have certificate data, use that as the end - if (this.Model.OptionalHeader?.CertificateTable != null) - { - int certificateTableAddress = (int)this.Model.OptionalHeader.CertificateTable.VirtualAddress.ConvertVirtualAddress(this.Model.SectionTable); - if (certificateTableAddress != 0 && certificateTableAddress < endOfFile) - endOfFile = certificateTableAddress; - } - - // Search through all sections and find the furthest a section goes - int endOfSectionData = -1; - foreach (var section in this.Model.SectionTable) - { - // If we have an invalid section - if (section == null) - continue; - - // If we have an invalid section address - int sectionAddress = (int)section.VirtualAddress.ConvertVirtualAddress(this.Model.SectionTable); - if (sectionAddress == 0) - continue; - - // If we have an invalid section size - if (section.SizeOfRawData == 0 && section.VirtualSize == 0) - continue; - - // Get the real section size - int sectionSize; - if (section.SizeOfRawData < section.VirtualSize) - sectionSize = (int)section.VirtualSize; - else - sectionSize = (int)section.SizeOfRawData; - - // Compare and set the end of section data - if (sectionAddress + sectionSize > endOfSectionData) - endOfSectionData = sectionAddress + sectionSize; - } - - // If we didn't find the end of section data - if (endOfSectionData <= 0) - return null; - - // If we're at the end of the file, cache an empty byte array - if (endOfSectionData >= endOfFile) - { - _overlayData = new byte[0]; - return _overlayData; - } - - // Otherwise, cache and return the data - int overlayLength = endOfFile - endOfSectionData; - _overlayData = ReadFromDataSource(endOfSectionData, overlayLength); - return _overlayData; - } - } - } - - /// - /// Overlay strings, if they exist - /// -#if NET48 - public List OverlayStrings -#else - public List? OverlayStrings -#endif - { - get - { - lock (_sourceDataLock) - { - // Use the cached data if possible - if (_overlayStrings != null) - return _overlayStrings; - - // Get the end of the file, if possible - int endOfFile = GetEndOfFile(); - if (endOfFile == -1) - return null; - - // If the section table is missing - if (this.Model.SectionTable == null) - return null; - - // If we have certificate data, use that as the end - if (this.Model.OptionalHeader?.CertificateTable != null) - { - int certificateTableAddress = (int)this.Model.OptionalHeader.CertificateTable.VirtualAddress.ConvertVirtualAddress(this.Model.SectionTable); - if (certificateTableAddress != 0 && certificateTableAddress < endOfFile) - endOfFile = certificateTableAddress; - } - - // Search through all sections and find the furthest a section goes - int endOfSectionData = -1; - foreach (var section in this.Model.SectionTable) - { - // If we have an invalid section - if (section == null) - continue; - - // If we have an invalid section address - int sectionAddress = (int)section.VirtualAddress.ConvertVirtualAddress(this.Model.SectionTable); - if (sectionAddress == 0) - continue; - - // If we have an invalid section size - if (section.SizeOfRawData == 0 && section.VirtualSize == 0) - continue; - - // Get the real section size - int sectionSize; - if (section.SizeOfRawData < section.VirtualSize) - sectionSize = (int)section.VirtualSize; - else - sectionSize = (int)section.SizeOfRawData; - - // Compare and set the end of section data - if (sectionAddress + sectionSize > endOfSectionData) - endOfSectionData = sectionAddress + sectionSize; - } - - // If we didn't find the end of section data - if (endOfSectionData <= 0) - return null; - - // If we're at the end of the file, cache an empty list - if (endOfSectionData >= endOfFile) - { - _overlayStrings = new List(); - return _overlayStrings; - } - - // Otherwise, cache and return the strings - int overlayLength = endOfFile - endOfSectionData; - _overlayStrings = ReadStringsFromDataSource(endOfSectionData, overlayLength, charLimit: 3); - return _overlayStrings; - } - } - } - - /// - /// Sanitized section names - /// -#if NET48 - public string[] SectionNames -#else - public string[]? SectionNames -#endif - { - get - { - lock (_sourceDataLock) - { - // Use the cached data if possible - if (_sectionNames != null) - return _sectionNames; - - // If there are no sections - if (this.Model.SectionTable == null) - return null; - - // Otherwise, build and return the cached array - _sectionNames = new string[this.Model.SectionTable.Length]; - for (int i = 0; i < _sectionNames.Length; i++) - { - var section = this.Model.SectionTable[i]; - if (section == null) - continue; - - // TODO: Handle long section names with leading `/` -#if NET48 - byte[] sectionNameBytes = section.Name; -#else - byte[]? sectionNameBytes = section.Name; -#endif - if (sectionNameBytes != null) - { - string sectionNameString = Encoding.UTF8.GetString(sectionNameBytes).TrimEnd('\0'); - _sectionNames[i] = sectionNameString; - } - } - - return _sectionNames; - } - } - } - - /// - /// Stub executable data, if it exists - /// -#if NET48 - public byte[] StubExecutableData -#else - public byte[]? StubExecutableData -#endif - { - get - { - lock (_sourceDataLock) - { - // If we already have cached data, just use that immediately - if (_stubExecutableData != null) - return _stubExecutableData; - - if (this.Model.Stub?.Header?.NewExeHeaderAddr == null) - return null; - - // Populate the raw stub executable data based on the source - int endOfStubHeader = 0x40; - int lengthOfStubExecutableData = (int)this.Model.Stub.Header.NewExeHeaderAddr - endOfStubHeader; - _stubExecutableData = ReadFromDataSource(endOfStubHeader, lengthOfStubExecutableData); - - // Cache and return the stub executable data, even if null - return _stubExecutableData; - } - } - } - - /// - /// Dictionary of debug data - /// -#if NET48 - public Dictionary DebugData -#else - public Dictionary? DebugData -#endif - { - get - { - lock (_sourceDataLock) - { - // Use the cached data if possible - if (_debugData != null && _debugData.Count != 0) - return _debugData; - - // If we have no resource table, just return - if (this.Model.DebugTable?.DebugDirectoryTable == null - || this.Model.DebugTable.DebugDirectoryTable.Length == 0) - return null; - - // Otherwise, build and return the cached dictionary - ParseDebugTable(); - return _debugData; - } - } - } - - /// - /// Dictionary of resource data - /// -#if NET48 - public Dictionary ResourceData -#else - public Dictionary? ResourceData -#endif - { - get - { - lock (_sourceDataLock) - { - // Use the cached data if possible - if (_resourceData != null && _resourceData.Count != 0) - return _resourceData; - - // If we have no resource table, just return - if (this.Model.OptionalHeader?.ResourceTable == null - || this.Model.OptionalHeader.ResourceTable.VirtualAddress == 0 - || this.Model.ResourceDirectoryTable == null) - return null; - - // Otherwise, build and return the cached dictionary - ParseResourceDirectoryTable(this.Model.ResourceDirectoryTable, types: new List()); - return _resourceData; - } - } - } - - #region Version Information - - /// - /// "Build GUID" - /// -#if NET48 - public string BuildGuid => GetVersionInfoString("BuildGuid"); -#else - public string? BuildGuid => GetVersionInfoString("BuildGuid"); -#endif - - /// - /// "Build signature" - /// -#if NET48 - public string BuildSignature => GetVersionInfoString("BuildSignature"); -#else - public string? BuildSignature => GetVersionInfoString("BuildSignature"); -#endif - - /// - /// Additional information that should be displayed for diagnostic purposes. - /// -#if NET48 - public string Comments => GetVersionInfoString("Comments"); -#else - public string? Comments => GetVersionInfoString("Comments"); -#endif - - /// - /// Company that produced the file—for example, "Microsoft Corporation" or - /// "Standard Microsystems Corporation, Inc." This string is required. - /// -#if NET48 - public string CompanyName => GetVersionInfoString("CompanyName"); -#else - public string? CompanyName => GetVersionInfoString("CompanyName"); -#endif - - /// - /// "Debug version" - /// -#if NET48 - public string DebugVersion => GetVersionInfoString("DebugVersion"); -#else - public string? DebugVersion => GetVersionInfoString("DebugVersion"); -#endif - - /// - /// File description to be presented to users. This string may be displayed in a - /// list box when the user is choosing files to install—for example, "Keyboard - /// Driver for AT-Style Keyboards". This string is required. - /// -#if NET48 - public string FileDescription => GetVersionInfoString("FileDescription"); -#else - public string? FileDescription => GetVersionInfoString("FileDescription"); -#endif - - /// - /// Version number of the file—for example, "3.10" or "5.00.RC2". This string - /// is required. - /// -#if NET48 - public string FileVersion => GetVersionInfoString("FileVersion"); -#else - public string? FileVersion => GetVersionInfoString("FileVersion"); -#endif - - /// - /// Internal name of the file, if one exists—for example, a module name if the - /// file is a dynamic-link library. If the file has no internal name, this - /// string should be the original filename, without extension. This string is required. - /// -#if NET48 - public string InternalName => GetVersionInfoString(key: "InternalName"); -#else - public string? InternalName => GetVersionInfoString(key: "InternalName"); -#endif - - /// - /// Copyright notices that apply to the file. This should include the full text of - /// all notices, legal symbols, copyright dates, and so on. This string is optional. - /// -#if NET48 - public string LegalCopyright => GetVersionInfoString(key: "LegalCopyright"); -#else - public string? LegalCopyright => GetVersionInfoString(key: "LegalCopyright"); -#endif - - /// - /// Trademarks and registered trademarks that apply to the file. This should include - /// the full text of all notices, legal symbols, trademark numbers, and so on. This - /// string is optional. - /// -#if NET48 - public string LegalTrademarks => GetVersionInfoString(key: "LegalTrademarks"); -#else - public string? LegalTrademarks => GetVersionInfoString(key: "LegalTrademarks"); -#endif - - /// - /// Original name of the file, not including a path. This information enables an - /// application to determine whether a file has been renamed by a user. The format of - /// the name depends on the file system for which the file was created. This string - /// is required. - /// -#if NET48 - public string OriginalFilename => GetVersionInfoString(key: "OriginalFilename"); -#else - public string? OriginalFilename => GetVersionInfoString(key: "OriginalFilename"); -#endif - - /// - /// Information about a private version of the file—for example, "Built by TESTER1 on - /// \TESTBED". This string should be present only if VS_FF_PRIVATEBUILD is specified in - /// the fileflags parameter of the root block. - /// -#if NET48 - public string PrivateBuild => GetVersionInfoString(key: "PrivateBuild"); -#else - public string? PrivateBuild => GetVersionInfoString(key: "PrivateBuild"); -#endif - - /// - /// "Product GUID" - /// -#if NET48 - public string ProductGuid => GetVersionInfoString("ProductGuid"); -#else - public string? ProductGuid => GetVersionInfoString("ProductGuid"); -#endif - - /// - /// Name of the product with which the file is distributed. This string is required. - /// -#if NET48 - public string ProductName => GetVersionInfoString(key: "ProductName"); -#else - public string? ProductName => GetVersionInfoString(key: "ProductName"); -#endif - - /// - /// Version of the product with which the file is distributed—for example, "3.10" or - /// "5.00.RC2". This string is required. - /// -#if NET48 - public string ProductVersion => GetVersionInfoString(key: "ProductVersion"); -#else - public string? ProductVersion => GetVersionInfoString(key: "ProductVersion"); -#endif - - /// - /// Text that specifies how this version of the file differs from the standard - /// version—for example, "Private build for TESTER1 solving mouse problems on M250 and - /// M250E computers". This string should be present only if VS_FF_SPECIALBUILD is - /// specified in the fileflags parameter of the root block. - /// -#if NET48 - public string SpecialBuild => GetVersionInfoString(key: "SpecialBuild") ?? GetVersionInfoString(key: "Special Build"); -#else - public string? SpecialBuild => GetVersionInfoString(key: "SpecialBuild") ?? GetVersionInfoString(key: "Special Build"); -#endif - - /// - /// "Trade name" - /// -#if NET48 - public string TradeName => GetVersionInfoString(key: "TradeName"); -#else - public string? TradeName => GetVersionInfoString(key: "TradeName"); -#endif - - /// - /// Get the internal version as reported by the resources - /// - /// Version string, null on error - /// The internal version is either the file version, product version, or assembly version, in that order -#if NET48 - public string GetInternalVersion() -#else - public string? GetInternalVersion() -#endif - { -#if NET48 - string version = this.FileVersion; -#else - string? version = this.FileVersion; -#endif - if (!string.IsNullOrWhiteSpace(version)) - return version.Replace(", ", "."); - - version = this.ProductVersion; - if (!string.IsNullOrWhiteSpace(version)) - return version.Replace(", ", "."); - - version = this.AssemblyVersion; - if (!string.IsNullOrWhiteSpace(version)) - return version; - - return null; - } - - #endregion - - #region Manifest Information - - /// - /// Description as derived from the assembly manifest - /// -#if NET48 - public string AssemblyDescription -#else - public string? AssemblyDescription -#endif - { - get - { - var manifest = GetAssemblyManifest(); - return manifest? - .Description? - .Value; - } - } - - /// - /// Version as derived from the assembly manifest - /// - /// - /// If there are multiple identities included in the manifest, - /// this will only retrieve the value from the first that doesn't - /// have a null or empty version. - /// -#if NET48 - public string AssemblyVersion -#else - public string? AssemblyVersion -#endif - { - get - { - var manifest = GetAssemblyManifest(); - return manifest? - .AssemblyIdentities? - .FirstOrDefault(ai => !string.IsNullOrWhiteSpace(ai?.Version))? - .Version; - } - } - - #endregion - - #endregion - - #region Instance Variables - - /// - /// Header padding data, if it exists - /// -#if NET48 - private byte[] _headerPaddingData = null; -#else - private byte[]? _headerPaddingData = null; -#endif - - /// - /// Header padding strings, if they exist - /// -#if NET48 - private List _headerPaddingStrings = null; -#else - private List? _headerPaddingStrings = null; -#endif - - /// - /// Entry point data, if it exists and isn't aligned to a section - /// -#if NET48 - private byte[] _entryPointData = null; -#else - private byte[]? _entryPointData = null; -#endif - - /// - /// Address of the overlay, if it exists - /// - private int? _overlayAddress = null; - - /// - /// Overlay data, if it exists - /// -#if NET48 - private byte[] _overlayData = null; -#else - private byte[]? _overlayData = null; -#endif - - /// - /// Overlay strings, if they exist - /// -#if NET48 - private List _overlayStrings = null; -#else - private List? _overlayStrings = null; -#endif - - /// - /// Stub executable data, if it exists - /// -#if NET48 - private byte[] _stubExecutableData = null; -#else - private byte[]? _stubExecutableData = null; -#endif - - /// - /// Sanitized section names - /// -#if NET48 - private string[] _sectionNames = null; -#else - private string[]? _sectionNames = null; -#endif - - /// - /// Cached raw section data - /// -#if NET48 - private byte[][] _sectionData = null; -#else - private byte[]?[]? _sectionData = null; -#endif - - /// - /// Cached found string data in sections - /// -#if NET48 - private List[] _sectionStringData = null; -#else - private List?[]? _sectionStringData = null; -#endif - - /// - /// Cached raw table data - /// -#if NET48 - private byte[][] _tableData = null; -#else - private byte[]?[]? _tableData = null; -#endif - - /// - /// Cached found string data in tables - /// -#if NET48 - private List[] _tableStringData = null; -#else - private List?[]? _tableStringData = null; -#endif - - /// - /// Cached debug data - /// - private readonly Dictionary _debugData = new Dictionary(); - - /// - /// Cached resource data - /// -#if NET48 - private readonly Dictionary _resourceData = new Dictionary(); -#else - private readonly Dictionary _resourceData = new Dictionary(); -#endif - - /// - /// Cached version info data - /// -#if NET48 - private SabreTools.Models.PortableExecutable.VersionInfo _versionInfo = null; -#else - private SabreTools.Models.PortableExecutable.VersionInfo? _versionInfo = null; -#endif - - /// - /// Cached assembly manifest data - /// -#if NET48 - private SabreTools.Models.PortableExecutable.AssemblyManifest _assemblyManifest = null; -#else - private SabreTools.Models.PortableExecutable.AssemblyManifest? _assemblyManifest = null; -#endif - - /// - /// Lock object for reading from the source - /// - private readonly object _sourceDataLock = new object(); - - #endregion - - #region Constructors - - /// -#if NET48 - public PortableExecutable(SabreTools.Models.PortableExecutable.Executable model, byte[] data, int offset) -#else - public PortableExecutable(SabreTools.Models.PortableExecutable.Executable? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public PortableExecutable(SabreTools.Models.PortableExecutable.Executable model, Stream data) -#else - public PortableExecutable(SabreTools.Models.PortableExecutable.Executable? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a PE executable from a byte array and offset - /// - /// Byte array representing the executable - /// Offset within the array to parse - /// A PE executable wrapper on success, null on failure -#if NET48 - public static PortableExecutable Create(byte[] data, int offset) -#else - public static PortableExecutable? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a PE executable from a Stream - /// - /// Stream representing the executable - /// A PE executable wrapper on success, null on failure -#if NET48 - public static PortableExecutable Create(Stream data) -#else - public static PortableExecutable? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var executable = new SabreTools.Serialization.Streams.PortableExecutable().Deserialize(data); - if (executable == null) - return null; - - try - { - return new PortableExecutable(executable, data); - } - catch - { - return null; - } - } - - #endregion - - #region Data - - // TODO: Cache all certificate objects - - /// - /// Get the version info string associated with a key, if possible - /// - /// Case-insensitive key to find in the version info - /// String representing the data, null on error - /// - /// This code does not take into account the locale and will find and return - /// the first available value. This may not actually matter for version info, - /// but it is worth mentioning. - /// -#if NET48 - public string GetVersionInfoString(string key) -#else - public string? GetVersionInfoString(string key) -#endif - { - // If we have an invalid key, we can't do anything - if (string.IsNullOrEmpty(key)) - return null; - - // Ensure that we have the resource data cached - if (ResourceData == null) - return null; - - // If we don't have string version info in this executable - var stringTable = _versionInfo?.StringFileInfo?.Children; - if (stringTable == null || !stringTable.Any()) - return null; - - // Try to find a key that matches - var match = stringTable - .SelectMany(st => st?.Children ?? Array.Empty()) - .FirstOrDefault(sd => sd != null && key.Equals(sd.Key, StringComparison.OrdinalIgnoreCase)); - - // Return either the match or null - return match?.Value?.TrimEnd('\0'); - } - - /// - /// Get the assembly manifest, if possible - /// - /// Assembly manifest object, null on error -#if NET48 - private SabreTools.Models.PortableExecutable.AssemblyManifest GetAssemblyManifest() -#else - private SabreTools.Models.PortableExecutable.AssemblyManifest? GetAssemblyManifest() -#endif - { - // Use the cached data if possible - if (_assemblyManifest != null) - return _assemblyManifest; - - // Ensure that we have the resource data cached - if (ResourceData == null) - return null; - - // Return the now-cached assembly manifest - return _assemblyManifest; - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.PortableExecutable.Print(builder, this.Model); - return builder; - } - - #endregion - - #region Debug Data - - /// - /// Find CodeView debug data by path - /// - /// Partial path to check for - /// Enumerable of matching debug data -#if NET48 - public IEnumerable FindCodeViewDebugTableByPath(string path) -#else - public IEnumerable FindCodeViewDebugTableByPath(string path) -#endif - { - // Ensure that we have the debug data cached - if (DebugData == null) -#if NET48 - return Enumerable.Empty(); -#else - return Enumerable.Empty(); -#endif - - var nb10Found = DebugData.Select(r => r.Value) - .Select(r => r as SabreTools.Models.PortableExecutable.NB10ProgramDatabase) - .Where(n => n != null) - .Where(n => n?.PdbFileName?.Contains(path) == true) - .Select(n => n as object); - - var rsdsFound = DebugData.Select(r => r.Value) - .Select(r => r as SabreTools.Models.PortableExecutable.RSDSProgramDatabase) - .Where(r => r != null) - .Where(r => r?.PathAndFileName?.Contains(path) == true) - .Select(r => r as object); - - return nb10Found.Concat(rsdsFound); - } - - /// - /// Find unparsed debug data by string value - /// - /// String value to check for - /// Enumerable of matching debug data -#if NET48 - public IEnumerable FindGenericDebugTableByValue(string value) -#else - public IEnumerable FindGenericDebugTableByValue(string value) -#endif - { - // Ensure that we have the resource data cached - if (DebugData == null) -#if NET48 - return Enumerable.Empty(); -#else - return Enumerable.Empty(); -#endif - - return DebugData.Select(r => r.Value) - .Select(b => b as byte[]) - .Where(b => b != null) - .Where(b => - { - try - { -#if NET48 - string arrayAsASCII = Encoding.ASCII.GetString(b); -#else - string? arrayAsASCII = Encoding.ASCII.GetString(b!); -#endif - if (arrayAsASCII.Contains(value)) - return true; - } - catch { } - - try - { -#if NET48 - string arrayAsUTF8 = Encoding.UTF8.GetString(b); -#else - string? arrayAsUTF8 = Encoding.UTF8.GetString(b!); -#endif - if (arrayAsUTF8.Contains(value)) - return true; - } - catch { } - - try - { -#if NET48 - string arrayAsUnicode = Encoding.Unicode.GetString(b); -#else - string? arrayAsUnicode = Encoding.Unicode.GetString(b!); -#endif - if (arrayAsUnicode.Contains(value)) - return true; - } - catch { } - - return false; - }); - } - - #endregion - - #region Debug Parsing - - /// - /// Parse the debug directory table information - /// - private void ParseDebugTable() - { - // If there is no debug table - if (this.Model.DebugTable?.DebugDirectoryTable == null) - return; - - // Loop through all debug table entries - for (int i = 0; i < this.Model.DebugTable.DebugDirectoryTable.Length; i++) - { - var entry = this.Model.DebugTable.DebugDirectoryTable[i]; - if (entry == null) - continue; - - uint address = entry.PointerToRawData; - uint size = entry.SizeOfData; - -#if NET48 - byte[] entryData = ReadFromDataSource((int)address, (int)size); -#else - byte[]? entryData = ReadFromDataSource((int)address, (int)size); -#endif - if (entryData == null) - continue; - - // If we have CodeView debug data, try to parse it - if (entry.DebugType == SabreTools.Models.PortableExecutable.DebugType.IMAGE_DEBUG_TYPE_CODEVIEW) - { - // Read the signature - int offset = 0; - uint signature = entryData.ReadUInt32(ref offset); - - // Reset the offset - offset = 0; - - // NB10 - if (signature == 0x3031424E) - { - var nb10ProgramDatabase = entryData.AsNB10ProgramDatabase(ref offset); - if (nb10ProgramDatabase != null) - { - _debugData[i] = nb10ProgramDatabase; - continue; - } - } - - // RSDS - else if (signature == 0x53445352) - { - var rsdsProgramDatabase = entryData.AsRSDSProgramDatabase(ref offset); - if (rsdsProgramDatabase != null) - { - _debugData[i] = rsdsProgramDatabase; - continue; - } - } - } - else - { - _debugData[i] = entryData; - } - } - } - - #endregion - - #region Resource Data - - /// - /// Find dialog box resources by title - /// - /// Dialog box title to check for - /// Enumerable of matching resources -#if NET48 - public IEnumerable FindDialogByTitle(string title) -#else - public IEnumerable FindDialogByTitle(string title) -#endif - { - // Ensure that we have the resource data cached - if (ResourceData == null) -#if NET48 - return Enumerable.Empty(); -#else - return Enumerable.Empty(); -#endif - - return ResourceData.Select(r => r.Value) - .Select(r => r as SabreTools.Models.PortableExecutable.DialogBoxResource) - .Where(d => d != null) - .Where(d => - { - return (d?.DialogTemplate?.TitleResource?.Contains(title) ?? false) - || (d?.ExtendedDialogTemplate?.TitleResource?.Contains(title) ?? false); - }); - } - - /// - /// Find dialog box resources by contained item title - /// - /// Dialog box item title to check for - /// Enumerable of matching resources -#if NET48 - public IEnumerable FindDialogBoxByItemTitle(string title) -#else - public IEnumerable FindDialogBoxByItemTitle(string title) -#endif - { - // Ensure that we have the resource data cached - if (ResourceData == null) -#if NET48 - return Enumerable.Empty(); -#else - return Enumerable.Empty(); -#endif - - return ResourceData.Select(r => r.Value) - .Select(r => r as SabreTools.Models.PortableExecutable.DialogBoxResource) - .Where(d => d != null) - .Where(d => - { - if (d?.DialogItemTemplates != null) - { - return d.DialogItemTemplates - .Where(dit => dit?.TitleResource != null) - .Any(dit => dit?.TitleResource?.Contains(title) == true); - } - else if (d?.ExtendedDialogItemTemplates != null) - { - return d.ExtendedDialogItemTemplates - .Where(edit => edit?.TitleResource != null) - .Any(edit => edit?.TitleResource?.Contains(title) == true); - } - - return false; - }); - } - - /// - /// Find string table resources by contained string entry - /// - /// String entry to check for - /// Enumerable of matching resources -#if NET48 - public IEnumerable> FindStringTableByEntry(string entry) -#else - public IEnumerable?> FindStringTableByEntry(string entry) -#endif - { - // Ensure that we have the resource data cached - if (ResourceData == null) -#if NET48 - return Enumerable.Empty>(); -#else - return Enumerable.Empty?>(); -#endif - - return ResourceData.Select(r => r.Value) -#if NET48 - .Select(r => r as Dictionary) -#else - .Select(r => r as Dictionary) -#endif - .Where(st => st != null) - .Where(st => st?.Select(kvp => kvp.Value)? - .Any(s => s != null && s.Contains(entry)) == true); - } - - /// - /// Find unparsed resources by type name - /// - /// Type name to check for - /// Enumerable of matching resources -#if NET48 - public IEnumerable FindResourceByNamedType(string typeName) -#else - public IEnumerable FindResourceByNamedType(string typeName) -#endif - { - // Ensure that we have the resource data cached - if (ResourceData == null) -#if NET48 - return Enumerable.Empty(); -#else - return Enumerable.Empty(); -#endif - - return ResourceData.Where(kvp => kvp.Key.Contains(typeName)) - .Select(kvp => kvp.Value as byte[]) - .Where(b => b != null); - } - - /// - /// Find unparsed resources by string value - /// - /// String value to check for - /// Enumerable of matching resources -#if NET48 - public IEnumerable FindGenericResource(string value) -#else - public IEnumerable FindGenericResource(string value) -#endif - { - // Ensure that we have the resource data cached - if (ResourceData == null) -#if NET48 - return Enumerable.Empty(); -#else - return Enumerable.Empty(); -#endif - - return ResourceData.Select(r => r.Value) - .Select(r => r as byte[]) - .Where(b => b != null) - .Where(b => - { - try - { -#if NET48 - string arrayAsASCII = Encoding.ASCII.GetString(b); -#else - string? arrayAsASCII = Encoding.ASCII.GetString(b!); -#endif - if (arrayAsASCII.Contains(value)) - return true; - } - catch { } - - try - { -#if NET48 - string arrayAsUTF8 = Encoding.UTF8.GetString(b); -#else - string? arrayAsUTF8 = Encoding.UTF8.GetString(b!); -#endif - if (arrayAsUTF8.Contains(value)) - return true; - } - catch { } - - try - { -#if NET48 - string arrayAsUnicode = Encoding.Unicode.GetString(b); -#else - string? arrayAsUnicode = Encoding.Unicode.GetString(b!); -#endif - if (arrayAsUnicode.Contains(value)) - return true; - } - catch { } - - return false; - }); - } - - #endregion - - #region Resource Parsing - - /// - /// Parse the resource directory table information - /// - private void ParseResourceDirectoryTable(SabreTools.Models.PortableExecutable.ResourceDirectoryTable table, List types) - { - if (table?.Entries == null) - return; - - for (int i = 0; i < table.Entries.Length; i++) - { - var entry = table.Entries[i]; - if (entry == null) - continue; - - var newTypes = new List(types ?? new List()); - - if (entry.Name?.UnicodeString != null) - newTypes.Add(Encoding.UTF8.GetString(entry.Name.UnicodeString)); - else - newTypes.Add(entry.IntegerID); - - ParseResourceDirectoryEntry(entry, newTypes); - } - } - - /// - /// Parse the name resource directory entry information - /// - private void ParseResourceDirectoryEntry(SabreTools.Models.PortableExecutable.ResourceDirectoryEntry entry, List types) - { - if (entry.DataEntry != null) - ParseResourceDataEntry(entry.DataEntry, types); - else if (entry.Subdirectory != null) - ParseResourceDirectoryTable(entry.Subdirectory, types); - } - - /// - /// Parse the resource data entry information - /// - /// - /// When caching the version information and assembly manifest, this code assumes that there is only one of each - /// of those resources in the entire exectuable. This means that only the last found version or manifest will - /// ever be cached. - /// - private void ParseResourceDataEntry(SabreTools.Models.PortableExecutable.ResourceDataEntry entry, List types) - { - // Create the key and value objects - string key = types == null ? $"UNKNOWN_{Guid.NewGuid()}" : string.Join(", ", types); -#if NET48 - object value = entry.Data; -#else - object? value = entry.Data; -#endif - - // If we have a known resource type - if (types != null && types.Count > 0 && types[0] is uint resourceType) - { - try - { - switch ((SabreTools.Models.PortableExecutable.ResourceType)resourceType) - { - case SabreTools.Models.PortableExecutable.ResourceType.RT_CURSOR: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_BITMAP: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_ICON: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_MENU: - value = entry.AsMenu(); - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_DIALOG: - value = entry.AsDialogBox(); - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_STRING: - value = entry.AsStringTable(); - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_FONTDIR: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_FONT: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_ACCELERATOR: - value = entry.AsAcceleratorTableResource(); - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_RCDATA: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_MESSAGETABLE: - value = entry.AsMessageResourceData(); - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_GROUP_CURSOR: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_GROUP_ICON: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_VERSION: - _versionInfo = entry.AsVersionInfo(); - value = _versionInfo; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_DLGINCLUDE: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_PLUGPLAY: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_VXD: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_ANICURSOR: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_ANIICON: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_HTML: - value = entry.Data; - break; - case SabreTools.Models.PortableExecutable.ResourceType.RT_MANIFEST: - _assemblyManifest = entry.AsAssemblyManifest(); - value = _versionInfo; - break; - default: - value = entry.Data; - break; - } - } - catch - { - // Fall back on byte array data for malformed items - value = entry.Data; - } - } - - // If we have a custom resource type - else if (types != null && types.Count > 0 && types[0] is string) - { - value = entry.Data; - } - - // Add the key and value to the cache - _resourceData[key] = value; - } - - #endregion - - #region Sections - - /// - /// Determine if a section is contained within the section table - /// - /// Name of the section to check for - /// True to enable exact matching of names, false for starts-with - /// True if the section is in the executable, false otherwise -#if NET48 - public bool ContainsSection(string sectionName, bool exact = false) -#else - public bool ContainsSection(string? sectionName, bool exact = false) -#endif - { - // If no section name is provided - if (sectionName == null) - return false; - - // Get all section names first - if (SectionNames == null) - return false; - - // If we're checking exactly, return only exact matches - if (exact) - return SectionNames.Any(n => n.Equals(sectionName)); - - // Otherwise, check if section name starts with the value - else - return SectionNames.Any(n => n.StartsWith(sectionName)); - } - - /// - /// Get the section index corresponding to the entry point, if possible - /// - /// Section index on success, null on error - public int FindEntryPointSectionIndex() - { - // If the section table is missing - if (this.Model.SectionTable == null) - return -1; - - // If the address is missing - if (this.Model.OptionalHeader?.AddressOfEntryPoint == null) - return -1; - - // If we don't have an entry point - if (this.Model.OptionalHeader.AddressOfEntryPoint.ConvertVirtualAddress(this.Model.SectionTable) == 0) - return -1; - - // Otherwise, find the section it exists within -#if NET48 - return this.Model.OptionalHeader.AddressOfEntryPoint.ContainingSectionIndex(this.Model.SectionTable); -#else - // Otherwise, find the section it exists within - return this.Model.OptionalHeader.AddressOfEntryPoint.ContainingSectionIndex(this.Model.SectionTable - .Where(sh => sh != null) - .Cast() - .ToArray()); -#endif - } - - /// - /// Get the first section based on name, if possible - /// - /// Name of the section to check for - /// True to enable exact matching of names, false for starts-with - /// Section data on success, null on error -#if NET48 - public SabreTools.Models.PortableExecutable.SectionHeader GetFirstSection(string name, bool exact = false) -#else - public SabreTools.Models.PortableExecutable.SectionHeader? GetFirstSection(string? name, bool exact = false) -#endif - { - // If we have no sections - if (SectionNames == null || !SectionNames.Any() || this.Model.SectionTable == null || !this.Model.SectionTable.Any()) - return null; - - // If the section doesn't exist - if (!ContainsSection(name, exact)) - return null; - - // Get the first index of the section - int index = Array.IndexOf(SectionNames, name); - if (index == -1) - return null; - - // Return the section - return this.Model.SectionTable[index]; - } - - /// - /// Get the last section based on name, if possible - /// - /// Name of the section to check for - /// True to enable exact matching of names, false for starts-with - /// Section data on success, null on error -#if NET48 - public SabreTools.Models.PortableExecutable.SectionHeader GetLastSection(string name, bool exact = false) -#else - public SabreTools.Models.PortableExecutable.SectionHeader? GetLastSection(string? name, bool exact = false) -#endif - { - // If we have no sections - if (SectionNames == null || !SectionNames.Any() || this.Model.SectionTable == null || !this.Model.SectionTable.Any()) - return null; - - // If the section doesn't exist - if (!ContainsSection(name, exact)) - return null; - - // Get the last index of the section - int index = Array.LastIndexOf(SectionNames, name); - if (index == -1) - return null; - - // Return the section - return this.Model.SectionTable[index]; - } - - /// - /// Get the section based on index, if possible - /// - /// Index of the section to check for - /// Section data on success, null on error -#if NET48 - public SabreTools.Models.PortableExecutable.SectionHeader GetSection(int index) -#else - public SabreTools.Models.PortableExecutable.SectionHeader? GetSection(int index) -#endif - { - // If we have no sections - if (this.Model.SectionTable == null || !this.Model.SectionTable.Any()) - return null; - - // If the section doesn't exist - if (index < 0 || index >= this.Model.SectionTable.Length) - return null; - - // Return the section - return this.Model.SectionTable[index]; - } - - /// - /// Get the first section data based on name, if possible - /// - /// Name of the section to check for - /// True to enable exact matching of names, false for starts-with - /// Section data on success, null on error -#if NET48 - public byte[] GetFirstSectionData(string name, bool exact = false) -#else - public byte[]? GetFirstSectionData(string? name, bool exact = false) -#endif - { - // If we have no sections - if (SectionNames == null || !SectionNames.Any() || this.Model.SectionTable == null || !this.Model.SectionTable.Any()) - return null; - - // If the section doesn't exist - if (!ContainsSection(name, exact)) - return null; - - // Get the first index of the section - int index = Array.IndexOf(SectionNames, name); - return GetSectionData(index); - } - - /// - /// Get the last section data based on name, if possible - /// - /// Name of the section to check for - /// True to enable exact matching of names, false for starts-with - /// Section data on success, null on error -#if NET48 - public byte[] GetLastSectionData(string name, bool exact = false) -#else - public byte[]? GetLastSectionData(string? name, bool exact = false) -#endif - { - // If we have no sections - if (SectionNames == null || !SectionNames.Any() || this.Model.SectionTable == null || !this.Model.SectionTable.Any()) - return null; - - // If the section doesn't exist - if (!ContainsSection(name, exact)) - return null; - - // Get the last index of the section - int index = Array.LastIndexOf(SectionNames, name); - return GetSectionData(index); - } - - /// - /// Get the section data based on index, if possible - /// - /// Index of the section to check for - /// Section data on success, null on error -#if NET48 - public byte[] GetSectionData(int index) -#else - public byte[]? GetSectionData(int index) -#endif - { - // If we have no sections - if (SectionNames == null || !SectionNames.Any() || this.Model.SectionTable == null || !this.Model.SectionTable.Any()) - return null; - - // If the section doesn't exist - if (index < 0 || index >= this.Model.SectionTable.Length) - return null; - - // Get the section data from the table - var section = this.Model.SectionTable[index]; - if (section == null) - return null; - - uint address = section.VirtualAddress.ConvertVirtualAddress(this.Model.SectionTable); - if (address == 0) - return null; - - // Set the section size - uint size = section.SizeOfRawData; - lock (_sourceDataLock) - { - // Create the section data array if we have to - if (_sectionData == null) - _sectionData = new byte[SectionNames.Length][]; - - // If we already have cached data, just use that immediately - if (_sectionData[index] != null) - return _sectionData[index]; - - // Populate the raw section data based on the source -#if NET48 - byte[] sectionData = ReadFromDataSource((int)address, (int)size); -#else - byte[]? sectionData = ReadFromDataSource((int)address, (int)size); -#endif - - // Cache and return the section data, even if null - _sectionData[index] = sectionData; - return sectionData; - } - } - - /// - /// Get the first section strings based on name, if possible - /// - /// Name of the section to check for - /// True to enable exact matching of names, false for starts-with - /// Section strings on success, null on error -#if NET48 - public List GetFirstSectionStrings(string name, bool exact = false) -#else - public List? GetFirstSectionStrings(string? name, bool exact = false) -#endif - { - // If we have no sections - if (SectionNames == null || !SectionNames.Any() || this.Model.SectionTable == null || !this.Model.SectionTable.Any()) - return null; - - // If the section doesn't exist - if (!ContainsSection(name, exact)) - return null; - - // Get the first index of the section - int index = Array.IndexOf(SectionNames, name); - return GetSectionStrings(index); - } - - /// - /// Get the last section strings based on name, if possible - /// - /// Name of the section to check for - /// True to enable exact matching of names, false for starts-with - /// Section strings on success, null on error -#if NET48 - public List GetLastSectionStrings(string name, bool exact = false) -#else - public List? GetLastSectionStrings(string? name, bool exact = false) -#endif - { - // If we have no sections - if (SectionNames == null || !SectionNames.Any() || this.Model.SectionTable == null || !this.Model.SectionTable.Any()) - return null; - - // If the section doesn't exist - if (!ContainsSection(name, exact)) - return null; - - // Get the last index of the section - int index = Array.LastIndexOf(SectionNames, name); - return GetSectionStrings(index); - } - - /// - /// Get the section strings based on index, if possible - /// - /// Index of the section to check for - /// Section strings on success, null on error -#if NET48 - public List GetSectionStrings(int index) -#else - public List? GetSectionStrings(int index) -#endif - { - // If we have no sections - if (SectionNames == null || !SectionNames.Any() || this.Model.SectionTable == null || !this.Model.SectionTable.Any()) - return null; - - // If the section doesn't exist - if (index < 0 || index >= this.Model.SectionTable.Length) - return null; - - // Get the section data from the table - var section = this.Model.SectionTable[index]; - if (section == null) - return null; - - uint address = section.VirtualAddress.ConvertVirtualAddress(this.Model.SectionTable); - if (address == 0) - return null; - - // Set the section size - uint size = section.SizeOfRawData; - lock (_sourceDataLock) - { - // Create the section string array if we have to - if (_sectionStringData == null) - _sectionStringData = new List[SectionNames.Length]; - - // If we already have cached data, just use that immediately - if (_sectionStringData[index] != null) - return _sectionStringData[index]; - - // Populate the section string data based on the source -#if NET48 - List sectionStringData = ReadStringsFromDataSource((int)address, (int)size); -#else - List? sectionStringData = ReadStringsFromDataSource((int)address, (int)size); -#endif - - // Cache and return the section string data, even if null - _sectionStringData[index] = sectionStringData; - return sectionStringData; - } - } - - #endregion - - #region Tables - - /// - /// Get the table data based on index, if possible - /// - /// Index of the table to check for - /// Table data on success, null on error -#if NET48 - public byte[] GetTableData(int index) -#else - public byte[]? GetTableData(int index) -#endif - { - // If the table doesn't exist - if (this.Model.OptionalHeader == null || index < 0 || index > 16) - return null; - - // Get the virtual address and size from the entries - uint virtualAddress = 0, size = 0; - switch (index) - { - case 1: - virtualAddress = this.Model.OptionalHeader.ExportTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ExportTable?.Size ?? 0; - break; - case 2: - virtualAddress = this.Model.OptionalHeader.ImportTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ImportTable?.Size ?? 0; - break; - case 3: - virtualAddress = this.Model.OptionalHeader.ResourceTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ResourceTable?.Size ?? 0; - break; - case 4: - virtualAddress = this.Model.OptionalHeader.ExceptionTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ExceptionTable?.Size ?? 0; - break; - case 5: - virtualAddress = this.Model.OptionalHeader.CertificateTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.CertificateTable?.Size ?? 0; - break; - case 6: - virtualAddress = this.Model.OptionalHeader.BaseRelocationTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.BaseRelocationTable?.Size ?? 0; - break; - case 7: - virtualAddress = this.Model.OptionalHeader.Debug?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.Debug?.Size ?? 0; - break; - case 8: // Architecture Table - virtualAddress = 0; - size = 0; - break; - case 9: - virtualAddress = this.Model.OptionalHeader.GlobalPtr?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.GlobalPtr?.Size ?? 0; - break; - case 10: - virtualAddress = this.Model.OptionalHeader.ThreadLocalStorageTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ThreadLocalStorageTable?.Size ?? 0; - break; - case 11: - virtualAddress = this.Model.OptionalHeader.LoadConfigTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.LoadConfigTable?.Size ?? 0; - break; - case 12: - virtualAddress = this.Model.OptionalHeader.BoundImport?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.BoundImport?.Size ?? 0; - break; - case 13: - virtualAddress = this.Model.OptionalHeader.ImportAddressTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ImportAddressTable?.Size ?? 0; - break; - case 14: - virtualAddress = this.Model.OptionalHeader.DelayImportDescriptor?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.DelayImportDescriptor?.Size ?? 0; - break; - case 15: - virtualAddress = this.Model.OptionalHeader.CLRRuntimeHeader?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.CLRRuntimeHeader?.Size ?? 0; - break; - case 16: // Reserved - virtualAddress = 0; - size = 0; - break; - } - - // If there is no section table - if (this.Model.SectionTable == null) - return null; - - // Get the physical address from the virtual one - uint address = virtualAddress.ConvertVirtualAddress(this.Model.SectionTable); - if (address == 0 || size == 0) - return null; - - lock (_sourceDataLock) - { - // Create the table data array if we have to - if (_tableData == null) - _tableData = new byte[16][]; - - // If we already have cached data, just use that immediately - if (_tableData[index] != null) - return _tableData[index]; - - // Populate the raw table data based on the source -#if NET48 - byte[] tableData = ReadFromDataSource((int)address, (int)size); -#else - byte[]? tableData = ReadFromDataSource((int)address, (int)size); -#endif - - // Cache and return the table data, even if null - _tableData[index] = tableData; - return tableData; - } - } - - /// - /// Get the table strings based on index, if possible - /// - /// Index of the table to check for - /// Table strings on success, null on error -#if NET48 - public List GetTableStrings(int index) -#else - public List? GetTableStrings(int index) -#endif - { - // If the table doesn't exist - if (this.Model.OptionalHeader == null || index < 0 || index > 16) - return null; - - // Get the virtual address and size from the entries - uint virtualAddress = 0, size = 0; - switch (index) - { - case 1: - virtualAddress = this.Model.OptionalHeader.ExportTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ExportTable?.Size ?? 0; - break; - case 2: - virtualAddress = this.Model.OptionalHeader.ImportTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ImportTable?.Size ?? 0; - break; - case 3: - virtualAddress = this.Model.OptionalHeader.ResourceTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ResourceTable?.Size ?? 0; - break; - case 4: - virtualAddress = this.Model.OptionalHeader.ExceptionTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ExceptionTable?.Size ?? 0; - break; - case 5: - virtualAddress = this.Model.OptionalHeader.CertificateTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.CertificateTable?.Size ?? 0; - break; - case 6: - virtualAddress = this.Model.OptionalHeader.BaseRelocationTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.BaseRelocationTable?.Size ?? 0; - break; - case 7: - virtualAddress = this.Model.OptionalHeader.Debug?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.Debug?.Size ?? 0; - break; - case 8: // Architecture Table - virtualAddress = 0; - size = 0; - break; - case 9: - virtualAddress = this.Model.OptionalHeader.GlobalPtr?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.GlobalPtr?.Size ?? 0; - break; - case 10: - virtualAddress = this.Model.OptionalHeader.ThreadLocalStorageTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ThreadLocalStorageTable?.Size ?? 0; - break; - case 11: - virtualAddress = this.Model.OptionalHeader.LoadConfigTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.LoadConfigTable?.Size ?? 0; - break; - case 12: - virtualAddress = this.Model.OptionalHeader.BoundImport?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.BoundImport?.Size ?? 0; - break; - case 13: - virtualAddress = this.Model.OptionalHeader.ImportAddressTable?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.ImportAddressTable?.Size ?? 0; - break; - case 14: - virtualAddress = this.Model.OptionalHeader.DelayImportDescriptor?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.DelayImportDescriptor?.Size ?? 0; - break; - case 15: - virtualAddress = this.Model.OptionalHeader.CLRRuntimeHeader?.VirtualAddress ?? 0; - size = this.Model.OptionalHeader.CLRRuntimeHeader?.Size ?? 0; - break; - case 16: // Reserved - virtualAddress = 0; - size = 0; - break; - } - - // If there is no section table - if (this.Model.SectionTable == null) - return null; - - // Get the physical address from the virtual one - uint address = virtualAddress.ConvertVirtualAddress(this.Model.SectionTable); - if (address == 0 || size == 0) - return null; - - lock (_sourceDataLock) - { - // Create the table string array if we have to - if (_tableStringData == null) - _tableStringData = new List[16]; - - // If we already have cached data, just use that immediately - if (_tableStringData[index] != null) - return _tableStringData[index]; - - // Populate the table string data based on the source -#if NET48 - List tableStringData = ReadStringsFromDataSource((int)address, (int)size); -#else - List? tableStringData = ReadStringsFromDataSource((int)address, (int)size); -#endif - - // Cache and return the table string data, even if null - _tableStringData[index] = tableStringData; - return tableStringData; - } - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/Printing.cs b/BinaryObjectScanner.Wrappers/Printing.cs new file mode 100644 index 00000000..afa08d3f --- /dev/null +++ b/BinaryObjectScanner.Wrappers/Printing.cs @@ -0,0 +1,298 @@ +using System.Text; +using SabreTools.Serialization.Interfaces; +using SabreTools.Serialization.Wrappers; + +namespace BinaryObjectScanner.Wrappers +{ + /// + /// Extensions to allow for pretty printing + /// + public static class PrintExtensions + { + /// + /// Export the item information as pretty-printed text + /// + public static StringBuilder PrettyPrint(this IWrapper wrapper) + { + switch (wrapper) + { + case AACSMediaKeyBlock item: return item.PrettyPrint(); + case BDPlusSVM item: return item.PrettyPrint(); + case BFPK item: return item.PrettyPrint(); + case BSP item: return item.PrettyPrint(); + case CFB item: return item.PrettyPrint(); + case CIA item: return item.PrettyPrint(); + case GCF item: return item.PrettyPrint(); + case InstallShieldCabinet item: return item.PrettyPrint(); + case LinearExecutable item: return item.PrettyPrint(); + case MicrosoftCabinet item: return item.PrettyPrint(); + case MSDOS item: return item.PrettyPrint(); + case N3DS item: return item.PrettyPrint(); + case NCF item: return item.PrettyPrint(); + case NewExecutable item: return item.PrettyPrint(); + case Nitro item: return item.PrettyPrint(); + case PAK item: return item.PrettyPrint(); + case PFF item: return item.PrettyPrint(); + case PlayJAudioFile item: return item.PrettyPrint(); + case PortableExecutable item: return item.PrettyPrint(); + case Quantum item: return item.PrettyPrint(); + case SGA item: return item.PrettyPrint(); + case VBSP item: return item.PrettyPrint(); + case VPK item: return item.PrettyPrint(); + case WAD item: return item.PrettyPrint(); + case XZP item: return item.PrettyPrint(); + default: return new StringBuilder(); + } + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this AACSMediaKeyBlock item) + { + StringBuilder builder = new StringBuilder(); + Printing.AACSMediaKeyBlock.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this BDPlusSVM item) + { + StringBuilder builder = new StringBuilder(); + Printing.BDPlusSVM.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this BFPK item) + { + StringBuilder builder = new StringBuilder(); + Printing.BFPK.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this BSP item) + { + StringBuilder builder = new StringBuilder(); + Printing.BSP.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this CFB item) + { + StringBuilder builder = new StringBuilder(); + Printing.CFB.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this CIA item) + { + StringBuilder builder = new StringBuilder(); + Printing.CIA.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this GCF item) + { + StringBuilder builder = new StringBuilder(); + Printing.GCF.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this InstallShieldCabinet item) + { + StringBuilder builder = new StringBuilder(); + Printing.InstallShieldCabinet.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this LinearExecutable item) + { + StringBuilder builder = new StringBuilder(); + Printing.LinearExecutable.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this MicrosoftCabinet item) + { + StringBuilder builder = new StringBuilder(); + Printing.MicrosoftCabinet.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this MSDOS item) + { + StringBuilder builder = new StringBuilder(); + Printing.MSDOS.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this N3DS item) + { + StringBuilder builder = new StringBuilder(); + Printing.N3DS.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this NCF item) + { + StringBuilder builder = new StringBuilder(); + Printing.NCF.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this NewExecutable item) + { + StringBuilder builder = new StringBuilder(); + Printing.NewExecutable.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this Nitro item) + { + StringBuilder builder = new StringBuilder(); + Printing.Nitro.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this PAK item) + { + StringBuilder builder = new StringBuilder(); + Printing.PAK.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this PFF item) + { + StringBuilder builder = new StringBuilder(); + Printing.PFF.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this PlayJAudioFile item) + { + StringBuilder builder = new StringBuilder(); + Printing.PlayJAudioFile.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this PortableExecutable item) + { + StringBuilder builder = new StringBuilder(); + Printing.PortableExecutable.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this Quantum item) + { + StringBuilder builder = new StringBuilder(); + Printing.Quantum.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this SGA item) + { + StringBuilder builder = new StringBuilder(); + Printing.SGA.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this VBSP item) + { + StringBuilder builder = new StringBuilder(); + Printing.VBSP.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this VPK item) + { + StringBuilder builder = new StringBuilder(); + Printing.VPK.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this WAD item) + { + StringBuilder builder = new StringBuilder(); + Printing.WAD.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this XZP item) + { + StringBuilder builder = new StringBuilder(); + Printing.XZP.Print(builder, item.Model); + return builder; + } + } +} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/Quantum.cs b/BinaryObjectScanner.Wrappers/Quantum.cs deleted file mode 100644 index 35c5aac3..00000000 --- a/BinaryObjectScanner.Wrappers/Quantum.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class Quantum : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Quantum Archive"; - - #endregion - - #region Constructors - - /// -#if NET48 - public Quantum(SabreTools.Models.Quantum.Archive model, byte[] data, int offset) -#else - public Quantum(SabreTools.Models.Quantum.Archive? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public Quantum(SabreTools.Models.Quantum.Archive model, Stream data) -#else - public Quantum(SabreTools.Models.Quantum.Archive? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a Quantum archive from a byte array and offset - /// - /// Byte array representing the archive - /// Offset within the array to parse - /// A Quantum archive wrapper on success, null on failure -#if NET48 - public static Quantum Create(byte[] data, int offset) -#else - public static Quantum? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a Quantum archive from a Stream - /// - /// Stream representing the archive - /// A Quantum archive wrapper on success, null on failure -#if NET48 - public static Quantum Create(Stream data) -#else - public static Quantum? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var archive = new SabreTools.Serialization.Streams.Quantum().Deserialize(data); - if (archive == null) - return null; - - try - { - return new Quantum(archive, data); - } - catch - { - return null; - } - } - - #endregion - - #region Data - - /// - /// Extract all files from the Quantum archive to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public bool ExtractAll(string outputDirectory) - { - // If we have no files - if (this.Model.FileList == null || this.Model.FileList.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.FileList.Length; i++) - { - allExtracted &= 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 bool ExtractFile(int index, string outputDirectory) - { - // If we have no files - if (this.Model.Header == null || this.Model.Header.FileCount == 0 || this.Model.FileList == null || this.Model.FileList.Length == 0) - return false; - - // If we have an invalid index - if (index < 0 || index >= this.Model.FileList.Length) - return false; - - // Get the file information - var fileDescriptor = this.Model.FileList[index]; - - // Read the entire compressed data - int compressedDataOffset = (int)this.Model.CompressedDataOffset; - int compressedDataLength = GetEndOfFile() - compressedDataOffset; -#if NET48 - byte[] compressedData = ReadFromDataSource(compressedDataOffset, compressedDataLength); -#else - byte[]? compressedData = 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 Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.Quantum.Print(builder, this.Model); - return builder; - } - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/SGA.cs b/BinaryObjectScanner.Wrappers/SGA.cs deleted file mode 100644 index de208a01..00000000 --- a/BinaryObjectScanner.Wrappers/SGA.cs +++ /dev/null @@ -1,352 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using ICSharpCode.SharpZipLib.Zip.Compression; - -namespace BinaryObjectScanner.Wrappers -{ - public class SGA : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "SGA"; - - #endregion - - #region Constructors - - /// -#if NET48 - public SGA(SabreTools.Models.SGA.File model, byte[] data, int offset) -#else - public SGA(SabreTools.Models.SGA.File? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public SGA(SabreTools.Models.SGA.File model, Stream data) -#else - public SGA(SabreTools.Models.SGA.File? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create an SGA from a byte array and offset - /// - /// Byte array representing the SGA - /// Offset within the array to parse - /// An SGA wrapper on success, null on failure -#if NET48 - public static SGA Create(byte[] data, int offset) -#else - public static SGA? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a SGA from a Stream - /// - /// Stream representing the SGA - /// An SGA wrapper on success, null on failure -#if NET48 - public static SGA Create(Stream data) -#else - public static SGA? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var file = new SabreTools.Serialization.Streams.SGA().Deserialize(data); - if (file == null) - return null; - - try - { - return new SGA(file, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.SGA.Print(builder, this.Model); - return builder; - } - - #endregion - - #region Extraction - - /// - /// Extract all files from the SGA to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public bool ExtractAll(string outputDirectory) - { - // Get the number of files - int filesLength; - switch (this.Model.Header?.MajorVersion) - { - case 4: filesLength = (Model.Directory as SabreTools.Models.SGA.Directory4)?.Files?.Length ?? 0; break; - case 5: filesLength = (Model.Directory as SabreTools.Models.SGA.Directory5)?.Files?.Length ?? 0; break; - case 6: filesLength = (Model.Directory as SabreTools.Models.SGA.Directory6)?.Files?.Length ?? 0; break; - case 7: filesLength = (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(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 bool ExtractFile(int index, string outputDirectory) - { - // Get the number of files - int filesLength; - switch (this.Model.Header?.MajorVersion) - { - case 4: filesLength = (Model.Directory as SabreTools.Models.SGA.Directory4)?.Files?.Length ?? 0; break; - case 5: filesLength = (Model.Directory as SabreTools.Models.SGA.Directory5)?.Files?.Length ?? 0; break; - case 6: filesLength = (Model.Directory as SabreTools.Models.SGA.Directory6)?.Files?.Length ?? 0; break; - case 7: filesLength = (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 (this.Model.Header?.MajorVersion) - { - case 4: file = (Model.Directory as SabreTools.Models.SGA.Directory4)?.Files?[index]; break; - case 5: file = (Model.Directory as SabreTools.Models.SGA.Directory5)?.Files?[index]; break; - case 6: file = (Model.Directory as SabreTools.Models.SGA.Directory6)?.Files?[index]; break; - case 7: file = (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 (this.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 (this.Model.Header?.MajorVersion) - { -#if NET48 - case 4: folder = (Model.Directory as SabreTools.Models.SGA.Directory4)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 5: folder = (Model.Directory as SabreTools.Models.SGA.Directory5)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 6: folder = (Model.Directory as SabreTools.Models.SGA.Directory6)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 7: folder = (Model.Directory as SabreTools.Models.SGA.Directory7)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; -#else - case 4: folder = (Model.Directory as SabreTools.Models.SGA.Directory4)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 5: folder = (Model.Directory as SabreTools.Models.SGA.Directory5)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 6: folder = (Model.Directory as SabreTools.Models.SGA.Directory6)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 7: folder = (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 (this.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 (this.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 (this.Model.Header?.MajorVersion) - { - case 4: fileOffset += (Model.Header as SabreTools.Models.SGA.Header4)?.FileDataOffset ?? 0; break; - case 5: fileOffset += (Model.Header as SabreTools.Models.SGA.Header4)?.FileDataOffset ?? 0; break; - case 6: fileOffset += (Model.Header as SabreTools.Models.SGA.Header6)?.FileDataOffset ?? 0; break; - case 7: fileOffset += (Model.Header as SabreTools.Models.SGA.Header6)?.FileDataOffset ?? 0; break; - default: return false; - }; - - // Get the file sizes - long fileSize, outputFileSize; - switch (this.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 = ReadFromDataSource((int)fileOffset, (int)fileSize); -#else - byte[]? compressedData = 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 - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/VBSP.cs b/BinaryObjectScanner.Wrappers/VBSP.cs deleted file mode 100644 index c4052f84..00000000 --- a/BinaryObjectScanner.Wrappers/VBSP.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System.IO; -using System.Text; -using static SabreTools.Models.VBSP.Constants; - -namespace BinaryObjectScanner.Wrappers -{ - public class VBSP : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Half-Life 2 Level (VBSP)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public VBSP(SabreTools.Models.VBSP.File model, byte[] data, int offset) -#else - public VBSP(SabreTools.Models.VBSP.File? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public VBSP(SabreTools.Models.VBSP.File model, Stream data) -#else - public VBSP(SabreTools.Models.VBSP.File? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a VBSP from a byte array and offset - /// - /// Byte array representing the VBSP - /// Offset within the array to parse - /// A VBSP wrapper on success, null on failure -#if NET48 - public static VBSP Create(byte[] data, int offset) -#else - public static VBSP? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a VBSP from a Stream - /// - /// Stream representing the VBSP - /// An VBSP wrapper on success, null on failure -#if NET48 - public static VBSP Create(Stream data) -#else - public static VBSP? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var file = new SabreTools.Serialization.Streams.VBSP().Deserialize(data); - if (file == null) - return null; - - try - { - return new VBSP(file, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.VBSP.Print(builder, this.Model); - return builder; - } - - #endregion - - #region Extraction - - /// - /// Extract all lumps from the VBSP to an output directory - /// - /// Output directory to write to - /// True if all lumps extracted, false otherwise - public bool ExtractAllLumps(string outputDirectory) - { - // If we have no lumps - if (this.Model.Header?.Lumps == null || this.Model.Header.Lumps.Length == 0) - return false; - - // Loop through and extract all lumps to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.Header.Lumps.Length; i++) - { - allExtracted &= 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 bool ExtractLump(int index, string outputDirectory) - { - // If we have no lumps - if (this.Model.Header?.Lumps == null || this.Model.Header.Lumps.Length == 0) - return false; - - // If the lumps index is invalid - if (index < 0 || index >= this.Model.Header.Lumps.Length) - return false; - - // Get the lump - var lump = this.Model.Header.Lumps[index]; - if (lump == null) - return false; - - // Read the data -#if NET48 - byte[] data = ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#else - byte[]? data = ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#endif - if (data == null) - return false; - - // Create the filename - string filename = $"lump_{index}.bin"; - switch (index) - { - case HL_VBSP_LUMP_ENTITIES: - filename = "entities.ent"; - break; - case 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 - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/VPK.cs b/BinaryObjectScanner.Wrappers/VPK.cs deleted file mode 100644 index 84eac02b..00000000 --- a/BinaryObjectScanner.Wrappers/VPK.cs +++ /dev/null @@ -1,338 +0,0 @@ -using System.IO; -using System.Linq; -using System.Text; -using SabreTools.IO; -using static SabreTools.Models.VPK.Constants; - -namespace BinaryObjectScanner.Wrappers -{ - public class VPK : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Valve Package File (VPK)"; - - #endregion - - #region Extension Properties - - /// - /// Array of archive filenames attached to the given VPK - /// -#if NET48 - public string[] ArchiveFilenames -#else - public string[]? ArchiveFilenames -#endif - { - get - { - // Use the cached value if we have it - if (_archiveFilenames != null) - return _archiveFilenames; - - // If we don't have a source filename - if (!(_streamData is FileStream fs) || string.IsNullOrWhiteSpace(fs.Name)) - return null; - - // If the filename is not the right format - string extension = Path.GetExtension(fs.Name).TrimStart('.'); -#if NET48 - string directoryName = Path.GetDirectoryName(fs.Name); -#else - string? directoryName = Path.GetDirectoryName(fs.Name); -#endif - string fileName = directoryName == null - ? Path.GetFileNameWithoutExtension(fs.Name) - : Path.Combine(directoryName, Path.GetFileNameWithoutExtension(fs.Name)); - - if (fileName.Length < 3) - return null; - else if (fileName.Substring(fileName.Length - 3) != "dir") - return null; - - // Get the archive count - int archiveCount = this.Model.DirectoryItems == null - ? 0 - : this.Model.DirectoryItems - .Select(di => di?.DirectoryEntry) - .Select(de => de?.ArchiveIndex ?? 0) - .Where(ai => ai != HL_VPK_NO_ARCHIVE) - .Max(); - - // Build the list of archive filenames to populate - _archiveFilenames = new string[archiveCount]; - - // Loop through and create the archive filenames - for (int i = 0; i < archiveCount; i++) - { - // We need 5 digits to print a short, but we already have 3 for dir. - string archiveFileName = $"{fileName.Substring(0, fileName.Length - 3)}{i.ToString().PadLeft(3, '0')}.{extension}"; - _archiveFilenames[i] = archiveFileName; - } - - // Return the array - return _archiveFilenames; - } - } - - #endregion - - #region Instance Variables - - /// - /// Array of archive filenames attached to the given VPK - /// -#if NET48 - private string[] _archiveFilenames = null; -#else - private string[]? _archiveFilenames = null; -#endif - - #endregion - - #region Constructors - - /// -#if NET48 - public VPK(SabreTools.Models.VPK.File model, byte[] data, int offset) -#else - public VPK(SabreTools.Models.VPK.File? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public VPK(SabreTools.Models.VPK.File model, Stream data) -#else - public VPK(SabreTools.Models.VPK.File? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a VPK from a byte array and offset - /// - /// Byte array representing the VPK - /// Offset within the array to parse - /// A VPK wrapper on success, null on failure -#if NET48 - public static VPK Create(byte[] data, int offset) -#else - public static VPK? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a VPK from a Stream - /// - /// Stream representing the VPK - /// A VPK wrapper on success, null on failure -#if NET48 - public static VPK Create(Stream data) -#else - public static VPK? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var file = new SabreTools.Serialization.Streams.VPK().Deserialize(data); - if (file == null) - return null; - - try - { - return new VPK(file, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.VPK.Print(builder, this.Model); - return builder; - } - - #endregion - - #region Extraction - - /// - /// Extract all files from the VPK to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public bool ExtractAll(string outputDirectory) - { - // If we have no directory items - if (this.Model.DirectoryItems == null || this.Model.DirectoryItems.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.DirectoryItems.Length; i++) - { - allExtracted &= 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 bool ExtractFile(int index, string outputDirectory) - { - // If we have no directory items - if (this.Model.DirectoryItems == null || this.Model.DirectoryItems.Length == 0) - return false; - - // If the directory item index is invalid - if (index < 0 || index >= this.Model.DirectoryItems.Length) - return false; - - // Get the directory item - var directoryItem = this.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 == HL_VPK_NO_ARCHIVE) - { - if (directoryItem.PreloadData == null) - return false; - - data = directoryItem.PreloadData; - } - else - { - // If we have invalid archives - if (ArchiveFilenames == null || ArchiveFilenames.Length == 0) - return false; - - // If we have an invalid index - if (directoryItem.DirectoryEntry.ArchiveIndex < 0 || directoryItem.DirectoryEntry.ArchiveIndex >= ArchiveFilenames.Length) - return false; - - // Get the archive filename - string archiveFileName = 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 - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/WAD.cs b/BinaryObjectScanner.Wrappers/WAD.cs deleted file mode 100644 index 798b50d6..00000000 --- a/BinaryObjectScanner.Wrappers/WAD.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.IO; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class WAD : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Half-Life Texture Package File (WAD)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public WAD(SabreTools.Models.WAD.File model, byte[] data, int offset) -#else - public WAD(SabreTools.Models.WAD.File? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public WAD(SabreTools.Models.WAD.File model, Stream data) -#else - public WAD(SabreTools.Models.WAD.File? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a WAD from a byte array and offset - /// - /// Byte array representing the WAD - /// Offset within the array to parse - /// A WAD wrapper on success, null on failure -#if NET48 - public static WAD Create(byte[] data, int offset) -#else - public static WAD? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a WAD from a Stream - /// - /// Stream representing the WAD - /// An WAD wrapper on success, null on failure -#if NET48 - public static WAD Create(Stream data) -#else - public static WAD? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var file = new SabreTools.Serialization.Streams.WAD().Deserialize(data); - if (file == null) - return null; - - try - { - return new WAD(file, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.WAD.Print(builder, this.Model); - return builder; - } - - #endregion - - #region Extraction - - /// - /// Extract all lumps from the WAD to an output directory - /// - /// Output directory to write to - /// True if all lumps extracted, false otherwise - public bool ExtractAllLumps(string outputDirectory) - { - // If we have no lumps - if (this.Model.Lumps == null || this.Model.Lumps.Length == 0) - return false; - - // Loop through and extract all lumps to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.Lumps.Length; i++) - { - allExtracted &= 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 bool ExtractLump(int index, string outputDirectory) - { - // If we have no lumps - if (this.Model.Lumps == null || this.Model.Lumps.Length == 0) - return false; - - // If the lumps index is invalid - if (index < 0 || index >= this.Model.Lumps.Length) - return false; - - // Get the lump - var lump = this.Model.Lumps[index]; - if (lump == null) - return false; - - // Read the data -- TODO: Handle uncompressed lumps (see BSP.ExtractTexture) -#if NET48 - byte[] data = ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#else - byte[]? data = 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 - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/WrapperBase.cs b/BinaryObjectScanner.Wrappers/WrapperBase.cs deleted file mode 100644 index 6e3732d4..00000000 --- a/BinaryObjectScanner.Wrappers/WrapperBase.cs +++ /dev/null @@ -1,419 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using SabreTools.IO; - -namespace BinaryObjectScanner.Wrappers -{ - public abstract class WrapperBase : IWrapper - { - #region Descriptive Properties - - /// - public string Description() => DescriptionString; - - /// - /// Description of the object - /// - public abstract string DescriptionString { get; } - - #endregion - - #region Properties - - /// - /// Internal model - /// -#if NET48 - public T Model { get; private set; } -#else - public T Model { get; init; } -#endif - - #endregion - - #region Instance Variables - - /// - /// Source of the original data - /// - protected DataSource _dataSource = DataSource.UNKNOWN; - - /// - /// Source byte array data - /// - /// This is only populated if is -#if NET48 - protected byte[] _byteArrayData = null; -#else - protected byte[]? _byteArrayData = null; -#endif - - /// - /// Source byte array data offset - /// - /// This is only populated if is - protected int _byteArrayOffset = -1; - - /// - /// Source Stream data - /// - /// This is only populated if is -#if NET48 - protected Stream _streamData = null; -#else - protected Stream? _streamData = null; -#endif - -#if NET6_0_OR_GREATER - - /// - /// JSON serializer options for output printing - /// - protected System.Text.Json.JsonSerializerOptions _jsonSerializerOptions - { - get - { - var serializer = new System.Text.Json.JsonSerializerOptions { IncludeFields = true, WriteIndented = true }; - serializer.Converters.Add(new ConcreteAbstractSerializer()); - serializer.Converters.Add(new ConcreteInterfaceSerializer()); - serializer.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); - return serializer; - } - } - -#endif - - #endregion - - #region Constructors - - /// - /// Construct a new instance of the wrapper from a byte array - /// -#if NET48 - protected WrapperBase(T model, byte[] data, int offset) -#else - protected WrapperBase(T? model, byte[]? data, int offset) -#endif - { - if (model == null) - throw new ArgumentNullException(nameof(model)); - if (data == null) - throw new ArgumentNullException(nameof(data)); - if (offset < 0 || offset >= data.Length) - throw new ArgumentOutOfRangeException(nameof(offset)); - - this.Model = model; - _dataSource = DataSource.ByteArray; - _byteArrayData = data; - _byteArrayOffset = offset; - } - - /// - /// Construct a new instance of the wrapper from a Stream - /// -#if NET48 - protected WrapperBase(T model, Stream data) -#else - protected WrapperBase(T? model, Stream? data) -#endif - { - if (model == null) - throw new ArgumentNullException(nameof(model)); - if (data == null) - throw new ArgumentNullException(nameof(data)); - if (data.Length == 0 || !data.CanSeek || !data.CanRead) - throw new ArgumentOutOfRangeException(nameof(data)); - - this.Model = model; - _dataSource = DataSource.Stream; - _streamData = data; - } - - #endregion - - #region Data - - /// - /// Validate the backing data source - /// - /// True if the data source is valid, false otherwise - protected bool DataSourceIsValid() - { - switch (_dataSource) - { - // Byte array data requires both a valid array and offset - case DataSource.ByteArray: - return _byteArrayData != null && _byteArrayOffset >= 0; - - // Stream data requires both a valid stream - case DataSource.Stream: - return _streamData != null && _streamData.CanRead && _streamData.CanSeek; - - // Everything else is invalid - case DataSource.UNKNOWN: - default: - return false; - } - } - - /// - /// Check if a data segment is valid in the data source - /// - /// Position in the source - /// Length of the data to check - /// True if the positional data is valid, false otherwise - protected bool SegmentValid(int position, int length) - { - // Validate the data souece - if (!DataSourceIsValid()) - return false; - - // If we have an invalid position - if (position < 0 || position >= GetEndOfFile()) - return false; - - switch (_dataSource) - { - case DataSource.ByteArray: -#if NET48 - return _byteArrayOffset + position + length <= _byteArrayData.Length; -#else - return _byteArrayOffset + position + length <= _byteArrayData!.Length; -#endif - - case DataSource.Stream: -#if NET48 - return position + length <= _streamData.Length; -#else - return position + length <= _streamData!.Length; -#endif - - // Everything else is invalid - case DataSource.UNKNOWN: - default: - return false; - } - } - - /// - /// Read data from the source - /// - /// Position in the source to read from - /// Length of the requested data - /// Byte array containing the requested data, null on error -#if NET48 - protected byte[] ReadFromDataSource(int position, int length) -#else - protected byte[]? ReadFromDataSource(int position, int length) -#endif - { - // Validate the data source - if (!DataSourceIsValid()) - return null; - - // Validate the requested segment - if (!SegmentValid(position, length)) - return null; - - // Read and return the data -#if NET48 - byte[] sectionData = null; -#else - byte[]? sectionData = null; -#endif - switch (_dataSource) - { - case DataSource.ByteArray: - sectionData = new byte[length]; -#if NET48 - Array.Copy(_byteArrayData, _byteArrayOffset + position, sectionData, 0, length); -#else - Array.Copy(_byteArrayData!, _byteArrayOffset + position, sectionData, 0, length); -#endif - break; - - case DataSource.Stream: -#if NET48 - long currentLocation = _streamData.Position; -#else - long currentLocation = _streamData!.Position; -#endif - _streamData.Seek(position, SeekOrigin.Begin); - sectionData = _streamData.ReadBytes(length); - _streamData.Seek(currentLocation, SeekOrigin.Begin); - break; - } - - return sectionData; - } - - /// - /// Read string data from the source - /// - /// Position in the source to read from - /// Length of the requested data - /// Number of characters needed to be a valid string - /// String list containing the requested data, null on error -#if NET48 - protected List ReadStringsFromDataSource(int position, int length, int charLimit = 5) -#else - protected List? ReadStringsFromDataSource(int position, int length, int charLimit = 5) -#endif - { - // Read the data as a byte array first -#if NET48 - byte[] sourceData = ReadFromDataSource(position, length); -#else - byte[]? sourceData = ReadFromDataSource(position, length); -#endif - if (sourceData == null) - return null; - - // If we have an invalid character limit, default to 5 - if (charLimit <= 0) - charLimit = 5; - - // Create the string list to return - var sourceStrings = new List(); - - // Setup cached data - int sourceDataIndex = 0; - string cachedString = string.Empty; - - // Check for ASCII strings - while (sourceDataIndex < sourceData.Length) - { - // If we have a control character or an invalid byte - if (sourceData[sourceDataIndex] < 0x20 || sourceData[sourceDataIndex] > 0x7F) - { - // If we have no cached string - if (cachedString.Length == 0) - { - sourceDataIndex++; - continue; - } - - // If we have a cached string greater than the limit - if (cachedString.Length >= charLimit) - sourceStrings.Add(cachedString); - - cachedString = string.Empty; - sourceDataIndex++; - continue; - } - - // All other characters get read in - cachedString += Encoding.ASCII.GetString(sourceData, sourceDataIndex, 1); - sourceDataIndex++; - } - - // If we have a cached string greater than the limit - if (cachedString.Length >= charLimit) - sourceStrings.Add(cachedString); - - // Reset cached data - sourceDataIndex = 0; - cachedString = string.Empty; - - // We are limiting the check for Unicode characters with a second byte of 0x00 for now - - // Check for Unicode strings - while (sourceDataIndex < sourceData.Length) - { - // Unicode characters are always 2 bytes - if (sourceDataIndex == sourceData.Length - 1) - break; - - ushort ch = BitConverter.ToUInt16(sourceData, sourceDataIndex); - - // If we have a null terminator or "invalid" character - if (ch == 0x0000 || (ch & 0xFF00) != 0) - { - // If we have no cached string - if (cachedString.Length == 0) - { - sourceDataIndex += 2; - continue; - } - - // If we have a cached string greater than the limit - if (cachedString.Length >= charLimit) - sourceStrings.Add(cachedString); - - cachedString = string.Empty; - sourceDataIndex += 2; - continue; - } - - // All other characters get read in - cachedString += Encoding.Unicode.GetString(sourceData, sourceDataIndex, 2); - sourceDataIndex += 2; - } - - // If we have a cached string greater than the limit - if (cachedString.Length >= charLimit) - sourceStrings.Add(cachedString); - - // Deduplicate the string list for storage - sourceStrings = sourceStrings.Distinct().OrderBy(s => s).ToList(); - - // TODO: Complete implementation of string finding - return sourceStrings; - } - - /// - /// Get the ending offset of the source - /// - /// Value greater than 0 for a valid end of file, -1 on error - protected int GetEndOfFile() - { - // Validate the data souece - if (!DataSourceIsValid()) - return -1; - - // Return the effective endpoint - switch (_dataSource) - { - case DataSource.ByteArray: -#if NET48 - return _byteArrayData.Length - _byteArrayOffset; -#else - return _byteArrayData!.Length - _byteArrayOffset; -#endif - - case DataSource.Stream: -#if NET48 - return (int)_streamData.Length; -#else - return (int)_streamData!.Length; -#endif - - case DataSource.UNKNOWN: - default: - return -1; - } - } - - #endregion - - #region Printing - - /// - /// Export the item information as pretty-printed text - /// - public abstract StringBuilder PrettyPrint(); - -#if NET6_0_OR_GREATER - /// - /// Export the item information as JSON - /// - public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions); -#endif - - #endregion - } -} \ No newline at end of file diff --git a/BinaryObjectScanner.Wrappers/WrapperFactory.cs b/BinaryObjectScanner.Wrappers/WrapperFactory.cs index 5740c502..d7ddd011 100644 --- a/BinaryObjectScanner.Wrappers/WrapperFactory.cs +++ b/BinaryObjectScanner.Wrappers/WrapperFactory.cs @@ -3,6 +3,8 @@ using System.IO; using BinaryObjectScanner.Matching; using BinaryObjectScanner.Utilities; using SabreTools.IO; +using SabreTools.Serialization.Interfaces; +using SabreTools.Serialization.Wrappers; namespace BinaryObjectScanner.Wrappers { diff --git a/BinaryObjectScanner.Wrappers/XZP.cs b/BinaryObjectScanner.Wrappers/XZP.cs deleted file mode 100644 index b06500f7..00000000 --- a/BinaryObjectScanner.Wrappers/XZP.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System.IO; -using System.Linq; -using System.Text; - -namespace BinaryObjectScanner.Wrappers -{ - public class XZP : WrapperBase - { - #region Descriptive Properties - - /// - public override string DescriptionString => "Xbox Package File (XZP)"; - - #endregion - - #region Constructors - - /// -#if NET48 - public XZP(SabreTools.Models.XZP.File model, byte[] data, int offset) -#else - public XZP(SabreTools.Models.XZP.File? model, byte[]? data, int offset) -#endif - : base(model, data, offset) - { - // All logic is handled by the base class - } - - /// -#if NET48 - public XZP(SabreTools.Models.XZP.File model, Stream data) -#else - public XZP(SabreTools.Models.XZP.File? model, Stream? data) -#endif - : base(model, data) - { - // All logic is handled by the base class - } - - /// - /// Create a XZP from a byte array and offset - /// - /// Byte array representing the XZP - /// Offset within the array to parse - /// A XZP wrapper on success, null on failure -#if NET48 - public static XZP Create(byte[] data, int offset) -#else - public static XZP? Create(byte[]? data, int offset) -#endif - { - // If the data is invalid - if (data == null) - return null; - - // If the offset is out of bounds - if (offset < 0 || offset >= data.Length) - return null; - - // Create a memory stream and use that - MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); - return Create(dataStream); - } - - /// - /// Create a XZP from a Stream - /// - /// Stream representing the XZP - /// A XZP wrapper on success, null on failure -#if NET48 - public static XZP Create(Stream data) -#else - public static XZP? Create(Stream? data) -#endif - { - // If the data is invalid - if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) - return null; - - var file = new SabreTools.Serialization.Streams.XZP().Deserialize(data); - if (file == null) - return null; - - try - { - return new XZP(file, data); - } - catch - { - return null; - } - } - - #endregion - - #region Printing - - /// - public override StringBuilder PrettyPrint() - { - StringBuilder builder = new StringBuilder(); - Printing.XZP.Print(builder, this.Model); - return builder; - } - - #endregion - - #region Extraction - - /// - /// Extract all files from the XZP to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public bool ExtractAll(string outputDirectory) - { - // If we have no directory entries - if (this.Model.DirectoryEntries == null || this.Model.DirectoryEntries.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < this.Model.DirectoryEntries.Length; i++) - { - allExtracted &= 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 bool ExtractFile(int index, string outputDirectory) - { - // If we have no directory entries - if (this.Model.DirectoryEntries == null || this.Model.DirectoryEntries.Length == 0) - return false; - - // If we have no directory items - if (this.Model.DirectoryItems == null || this.Model.DirectoryItems.Length == 0) - return false; - - // If the directory entry index is invalid - if (index < 0 || index >= this.Model.DirectoryEntries.Length) - return false; - - // Get the directory entry - var directoryEntry = this.Model.DirectoryEntries[index]; - if (directoryEntry == null) - return false; - - // Get the associated directory item - var directoryItem = this.Model.DirectoryItems.Where(di => di?.FileNameCRC == directoryEntry.FileNameCRC).FirstOrDefault(); - if (directoryItem == null) - return false; - - // Load the item data -#if NET48 - byte[] data = ReadFromDataSource((int)directoryEntry.EntryOffset, (int)directoryEntry.EntryLength); -#else - byte[]? data = 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/BurnOutSharp/BurnOutSharp.csproj b/BurnOutSharp/BurnOutSharp.csproj index 577994ed..8a786773 100644 --- a/BurnOutSharp/BurnOutSharp.csproj +++ b/BurnOutSharp/BurnOutSharp.csproj @@ -21,7 +21,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Test/Test.csproj b/Test/Test.csproj index f459fe19..dd6db271 100644 --- a/Test/Test.csproj +++ b/Test/Test.csproj @@ -18,8 +18,8 @@ - - + +