diff --git a/BurnOutSharp.Builders/XZP.cs b/BurnOutSharp.Builders/XZP.cs index feaeb1f6..c46fd0d8 100644 --- a/BurnOutSharp.Builders/XZP.cs +++ b/BurnOutSharp.Builders/XZP.cs @@ -131,7 +131,7 @@ namespace BurnOutSharp.Builders file.DirectoryItems = new DirectoryItem[header.DirectoryItemCount]; // Try to parse the directory items - for (int i = 0; i < header.PreloadDirectoryEntryCount; i++) + for (int i = 0; i < header.DirectoryItemCount; i++) { var directoryItem = ParseDirectoryItem(data); file.DirectoryItems[i] = directoryItem; @@ -234,6 +234,18 @@ namespace BurnOutSharp.Builders directoryItem.NameOffset = data.ReadUInt32(); directoryItem.TimeCreated = data.ReadUInt32(); + // Cache the current offset + long currentPosition = data.Position; + + // Seek to the name offset + data.Seek(directoryItem.NameOffset, SeekOrigin.Begin); + + // Read the name + directoryItem.Name = data.ReadString(Encoding.ASCII); + + // Seek back to the right position + data.Seek(currentPosition, SeekOrigin.Begin); + return directoryItem; } diff --git a/BurnOutSharp.Models/XZP/DirectoryItem.cs b/BurnOutSharp.Models/XZP/DirectoryItem.cs index 6ccdcdc6..d051e9cb 100644 --- a/BurnOutSharp.Models/XZP/DirectoryItem.cs +++ b/BurnOutSharp.Models/XZP/DirectoryItem.cs @@ -7,6 +7,8 @@ namespace BurnOutSharp.Models.XZP public uint NameOffset; + public string Name; + public uint TimeCreated; } } diff --git a/BurnOutSharp.Wrappers/XZP.cs b/BurnOutSharp.Wrappers/XZP.cs new file mode 100644 index 00000000..011de545 --- /dev/null +++ b/BurnOutSharp.Wrappers/XZP.cs @@ -0,0 +1,388 @@ +using System; +using System.IO; +using System.Linq; + +namespace BurnOutSharp.Wrappers +{ + public class XZP : WrapperBase + { + #region Pass-Through Properties + + #region Header + + /// + public string Signature => _file.Header.Signature; + + /// + public uint Version => _file.Header.Version; + + /// + public uint PreloadDirectoryEntryCount => _file.Header.PreloadDirectoryEntryCount; + + /// + public uint DirectoryEntryCount => _file.Header.DirectoryEntryCount; + + /// + public uint PreloadBytes => _file.Header.PreloadBytes; + + /// + public uint HeaderLength => _file.Header.HeaderLength; + + /// + public uint DirectoryItemCount => _file.Header.DirectoryItemCount; + + /// + public uint DirectoryItemOffset => _file.Header.DirectoryItemOffset; + + /// + public uint DirectoryItemLength => _file.Header.DirectoryItemLength; + + #endregion + + #region Directory Entries + + /// + public Models.XZP.DirectoryEntry[] DirectoryEntries => _file.DirectoryEntries; + + #endregion + + #region Preload Directory Entries + + /// + public Models.XZP.DirectoryEntry[] PreloadDirectoryEntries => _file.PreloadDirectoryEntries; + + #endregion + + #region Preload Directory Entries + + /// + public Models.XZP.DirectoryMapping[] PreloadDirectoryMappings => _file.PreloadDirectoryMappings; + + #endregion + + #region Directory Items + + /// + public Models.XZP.DirectoryItem[] DirectoryItems => _file.DirectoryItems; + + #endregion + + #region Footer + + /// + public uint F_FileLength => _file.Footer.FileLength; + + /// + public string F_Signature => _file.Footer.Signature; + + #endregion + + #endregion + + #region Extension Properties + + // TODO: Figure out what extensions are needed + + #endregion + + #region Instance Variables + + /// + /// Internal representation of the XZP + /// + private Models.XZP.File _file; + + #endregion + + #region Constructors + + /// + /// Private constructor + /// + private XZP() { } + + /// + /// 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 + public static XZP Create(byte[] data, int offset) + { + // 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 + public static XZP Create(Stream data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + var file = Builders.XZP.ParseFile(data); + if (file == null) + return null; + + var wrapper = new XZP + { + _file = file, + _dataSource = DataSource.Stream, + _streamData = data, + }; + return wrapper; + } + + #endregion + + #region Printing + + /// + public override void Print() + { + Console.WriteLine("XZP Information:"); + Console.WriteLine("-------------------------"); + Console.WriteLine(); + + PrintHeader(); + PrintDirectoryEntries(); + PrintPreloadDirectoryEntries(); + PrintPreloadDirectoryMappings(); + PrintDirectoryItems(); + PrintFooter(); + } + + /// + /// Print header information + /// + private void PrintHeader() + { + Console.WriteLine(" Header Information:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" Signature: {Signature}"); + Console.WriteLine($" Version: {Version}"); + Console.WriteLine($" Preload directory entry count: {PreloadDirectoryEntryCount}"); + Console.WriteLine($" Directory entry count: {DirectoryEntryCount}"); + Console.WriteLine($" Preload bytes: {PreloadBytes}"); + Console.WriteLine($" Header length: {HeaderLength}"); + Console.WriteLine($" Directory item count: {DirectoryItemCount}"); + Console.WriteLine($" Directory item offset: {DirectoryItemOffset}"); + Console.WriteLine($" Directory item length: {DirectoryItemLength}"); + Console.WriteLine(); + } + + /// + /// Print directory entries information + /// + private void PrintDirectoryEntries() + { + Console.WriteLine(" Directory Entries Information:"); + Console.WriteLine(" -------------------------"); + if (DirectoryEntries == null || DirectoryEntries.Length == 0) + { + Console.WriteLine(" No directory entries"); + } + else + { + for (int i = 0; i < DirectoryEntries.Length; i++) + { + var directoryEntry = DirectoryEntries[i]; + Console.WriteLine($" Directory Entry {i}"); + Console.WriteLine($" File name CRC: {directoryEntry.FileNameCRC}"); + Console.WriteLine($" Entry length: {directoryEntry.EntryLength}"); + Console.WriteLine($" Entry offset: {directoryEntry.EntryOffset}"); + } + } + Console.WriteLine(); + } + + /// + /// Print preload directory entries information + /// + private void PrintPreloadDirectoryEntries() + { + Console.WriteLine(" Preload Directory Entries Information:"); + Console.WriteLine(" -------------------------"); + if (PreloadDirectoryEntries == null || PreloadDirectoryEntries.Length == 0) + { + Console.WriteLine(" No preload directory entries"); + } + else + { + for (int i = 0; i < PreloadDirectoryEntries.Length; i++) + { + var preloadDirectoryEntry = PreloadDirectoryEntries[i]; + Console.WriteLine($" Directory Entry {i}"); + Console.WriteLine($" File name CRC: {preloadDirectoryEntry.FileNameCRC}"); + Console.WriteLine($" Entry length: {preloadDirectoryEntry.EntryLength}"); + Console.WriteLine($" Entry offset: {preloadDirectoryEntry.EntryOffset}"); + } + } + Console.WriteLine(); + } + + /// + /// Print preload directory mappings information + /// + private void PrintPreloadDirectoryMappings() + { + Console.WriteLine(" Preload Directory Mappings Information:"); + Console.WriteLine(" -------------------------"); + if (PreloadDirectoryMappings == null || PreloadDirectoryMappings.Length == 0) + { + Console.WriteLine(" No preload directory mappings"); + } + else + { + for (int i = 0; i < PreloadDirectoryMappings.Length; i++) + { + var preloadDirectoryMapping = PreloadDirectoryMappings[i]; + Console.WriteLine($" Directory Mapping {i}"); + Console.WriteLine($" Preload directory entry index: {preloadDirectoryMapping.PreloadDirectoryEntryIndex}"); + } + } + Console.WriteLine(); + } + + /// + /// Print directory items information + /// + private void PrintDirectoryItems() + { + Console.WriteLine(" Directory Items Information:"); + Console.WriteLine(" -------------------------"); + if (DirectoryItems == null || DirectoryItems.Length == 0) + { + Console.WriteLine(" No directory items"); + } + else + { + for (int i = 0; i < DirectoryItems.Length; i++) + { + var directoryItem = DirectoryItems[i]; + Console.WriteLine($" Directory Item {i}"); + Console.WriteLine($" File name CRC: {directoryItem.FileNameCRC}"); + Console.WriteLine($" Name offset: {directoryItem.NameOffset}"); + Console.WriteLine($" Name: {directoryItem.Name ?? "[NULL]"}"); + Console.WriteLine($" Time created: {directoryItem.TimeCreated}"); + } + } + Console.WriteLine(); + } + + /// + /// Print footer information + /// + private void PrintFooter() + { + Console.WriteLine(" Footer Information:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" File length: {F_FileLength}"); + Console.WriteLine($" Signature: {F_Signature}"); + Console.WriteLine(); + } + + #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 (DirectoryEntries == null || DirectoryEntries.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < 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 (DirectoryEntries == null || DirectoryEntries.Length == 0) + return false; + + // If we have no directory items + if (DirectoryItems == null || DirectoryItems.Length == 0) + return false; + + // If the directory entry index is invalid + if (index < 0 || index >= DirectoryEntries.Length) + return false; + + // Get the directory entry + var directoryEntry = DirectoryEntries[index]; + if (directoryEntry == null) + return false; + + // Get the associated directory item + var directoryItem = DirectoryItems.Where(di => di.FileNameCRC == directoryEntry.FileNameCRC).FirstOrDefault(); + if (directoryItem == null) + return false; + + // Load the item data + byte[] data = ReadFromDataSource((int)directoryEntry.EntryOffset, (int)directoryEntry.EntryLength); + + // Create the filename + string filename = directoryItem.Name; + + // 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 + } +} \ No newline at end of file diff --git a/BurnOutSharp/FileType/XZP.cs b/BurnOutSharp/FileType/XZP.cs new file mode 100644 index 00000000..967a8523 --- /dev/null +++ b/BurnOutSharp/FileType/XZP.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using BurnOutSharp.Interfaces; +using static BurnOutSharp.Utilities.Dictionary; + +namespace BurnOutSharp.FileType +{ + /// + /// Valve Package File + /// + public class XZP : IScannable + { + /// + public ConcurrentDictionary> Scan(Scanner scanner, string file) + { + if (!File.Exists(file)) + return null; + + using (var fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + return Scan(scanner, fs, file); + } + } + + /// + public ConcurrentDictionary> Scan(Scanner scanner, Stream stream, string file) + { + // If the XZP file itself fails + try + { + string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempPath); + + // Create the wrapper + Wrappers.XZP xzp = Wrappers.XZP.Create(stream); + if (xzp == null) + return null; + + // Loop through and extract all files + xzp.ExtractAll(tempPath); + + // Collect and format all found protections + var protections = scanner.GetProtections(tempPath); + + // If temp directory cleanup fails + try + { + Directory.Delete(tempPath, true); + } + catch (Exception ex) + { + if (scanner.IncludeDebug) Console.WriteLine(ex); + } + + // Remove temporary path references + StripFromKeys(protections, tempPath); + + return protections; + } + catch (Exception ex) + { + if (scanner.IncludeDebug) Console.WriteLine(ex); + } + + return null; + } + } +} diff --git a/BurnOutSharp/Tools/Utilities.cs b/BurnOutSharp/Tools/Utilities.cs index cbea2e3f..a731317a 100644 --- a/BurnOutSharp/Tools/Utilities.cs +++ b/BurnOutSharp/Tools/Utilities.cs @@ -7,7 +7,7 @@ using BurnOutSharp.Wrappers; namespace BurnOutSharp.Tools { - internal static class Utilities + public static class Utilities { #region File Types @@ -647,7 +647,7 @@ namespace BurnOutSharp.Tools case SupportedFileType.VPK: return new FileType.VPK(); case SupportedFileType.WAD: return new FileType.WAD(); case SupportedFileType.XZ: return new FileType.XZ(); - case SupportedFileType.XZP: return new FileType.Valve(); + case SupportedFileType.XZP: return new FileType.XZP(); default: return null; } } diff --git a/Test/Program.cs b/Test/Program.cs index 91db446f..f648f5e6 100644 --- a/Test/Program.cs +++ b/Test/Program.cs @@ -213,8 +213,11 @@ namespace Test byte[] magic = stream.ReadBytes(8); stream.Seek(0, SeekOrigin.Begin); + // Get the file type + SupportedFileType ft = BurnOutSharp.Tools.Utilities.GetFileType(magic); + // MS-DOS executable and decendents - if (IsMSDOS(magic)) + if (ft == SupportedFileType.Executable) { // Build the executable information Console.WriteLine("Creating MS-DOS executable builder"); @@ -293,7 +296,7 @@ namespace Test } // BFPK archive - else if (IsBFPK(magic)) + else if (ft == SupportedFileType.BFPK) { // Build the BFPK information Console.WriteLine("Creating BFPK deserializer"); @@ -312,7 +315,7 @@ namespace Test } // BSP - else if (IsBSP(magic)) + else if (ft == SupportedFileType.BSP) { // Build the BSP information Console.WriteLine("Creating BSP deserializer"); @@ -331,7 +334,7 @@ namespace Test } // GCF - else if (IsGCF(magic)) + else if (ft == SupportedFileType.GCF) { // Build the GCF information Console.WriteLine("Creating GCF deserializer"); @@ -350,7 +353,7 @@ namespace Test } // MoPaQ (MPQ) archive - else if (IsMoPaQ(magic)) + else if (ft == SupportedFileType.MPQ) { // Build the archive information Console.WriteLine("Creating MoPaQ deserializer"); @@ -363,7 +366,7 @@ namespace Test } // MS-CAB archive - else if (IsMSCAB(magic)) + else if (ft == SupportedFileType.MicrosoftCAB) { // Build the cabinet information Console.WriteLine("Creating MS-CAB deserializer"); @@ -382,7 +385,7 @@ namespace Test } // NCF - else if (IsNCF(magic)) + else if (ft == SupportedFileType.NCF) { // Build the NCF information Console.WriteLine("Creating NCF deserializer"); @@ -401,7 +404,7 @@ namespace Test } // PAK - else if (IsPAK(magic)) + else if (ft == SupportedFileType.PAK) { // Build the archive information Console.WriteLine("Creating PAK deserializer"); @@ -420,7 +423,7 @@ namespace Test } // VBSP - else if (IsVBSP(magic)) + else if (ft == SupportedFileType.VBSP) { // Build the archive information Console.WriteLine("Creating VBSP deserializer"); @@ -439,7 +442,7 @@ namespace Test } // VPK - else if (IsVPK(magic)) + else if (ft == SupportedFileType.VPK) { // Build the archive information Console.WriteLine("Creating VPK deserializer"); @@ -458,7 +461,7 @@ namespace Test } // WAD - else if (IsWAD(magic)) + else if (ft == SupportedFileType.WAD) { // Build the archive information Console.WriteLine("Creating WAD deserializer"); @@ -476,6 +479,25 @@ namespace Test wad.Print(); } + // XZP + else if (ft == SupportedFileType.XZP) + { + // Build the archive information + Console.WriteLine("Creating XZP deserializer"); + Console.WriteLine(); + + var xzp = XZP.Create(stream); + if (xzp == null) + { + Console.WriteLine("Something went wrong parsing XZP"); + Console.WriteLine(); + return; + } + + // Print the XZP info to screen + xzp.Print(); + } + // Everything else else { @@ -486,85 +508,6 @@ namespace Test } } - /// - /// Determine if the magic bytes indicate an BFPK archive - /// - private static bool IsBFPK(byte[] magic) - { - if (magic == null || magic.Length < 4) - return false; - - return magic[0] == 'B' && magic[1] == 'F' && magic[2] == 'P' && magic[3] == 'K'; - } - - /// - /// Determine if the magic bytes indicate a BSP - /// - private static bool IsBSP(byte[] magic) - { - if (magic == null || magic.Length < 4) - return false; - - return magic[0] == 0x1e && magic[1] == 0x00 && magic[2] == 0x00 && magic[3] == 0x00; - } - - /// - /// Determine if the magic bytes indicate a GCF - /// - private static bool IsGCF(byte[] magic) - { - if (magic == null || magic.Length < 8) - return false; - - return magic[0] == 0x01 && magic[1] == 0x00 && magic[2] == 0x00 && magic[3] == 0x00 - && magic[4] == 0x01 && magic[5] == 0x00 && magic[6] == 0x00 && magic[7] == 0x00; - } - - /// - /// Determine if the magic bytes indicate an MoPaQ archive - /// - private static bool IsMoPaQ(byte[] magic) - { - if (magic == null || magic.Length < 4) - return false; - - return magic[0] == 'M' && magic[1] == 'P' && magic[2] == 'Q' && (magic[3] == 0x1A || magic[3] == 0x1B); - } - - /// - /// Determine if the magic bytes indicate an MS-CAB archive - /// - private static bool IsMSCAB(byte[] magic) - { - if (magic == null || magic.Length < 4) - return false; - - return magic[0] == 'M' && magic[1] == 'S' && magic[2] == 'C' && magic[3] == 'F'; - } - - /// - /// Determine if the magic bytes indicate an MS-DOS executable - /// - private static bool IsMSDOS(byte[] magic) - { - if (magic == null || magic.Length < 2) - return false; - - return magic[0] == 'M' && magic[1] == 'Z'; - } - - /// - /// Determine if the magic bytes indicate an NCF - /// - private static bool IsNCF(byte[] magic) - { - if (magic == null || magic.Length < 8) - return false; - - return magic[0] == 0x01 && magic[1] == 0x00 && magic[2] == 0x00 && magic[3] == 0x00 - && magic[4] == 0x02 && magic[5] == 0x00 && magic[6] == 0x00 && magic[7] == 0x00; - } - /// /// Determine if the magic bytes indicate a New Executable /// @@ -587,17 +530,6 @@ namespace Test return magic[0] == 'L' && (magic[1] == 'E' || magic[1] == 'X'); } - /// - /// Determine if the magic bytes indicate a PAK - /// - private static bool IsPAK(byte[] magic) - { - if (magic == null || magic.Length < 4) - return false; - - return magic[0] == 'P' && magic[1] == 'A' && magic[2] == 'C' && magic[3] == 'K'; - } - /// /// Determine if the magic bytes indicate a Portable Executable /// @@ -609,39 +541,6 @@ namespace Test return magic[0] == 'P' && magic[1] == 'E' && magic[2] == '\0' && magic[3] == '\0'; } - /// - /// Determine if the magic bytes indicate a VBSP - /// - private static bool IsVBSP(byte[] magic) - { - if (magic == null || magic.Length < 4) - return false; - - return magic[0] == 'V' && magic[1] == 'B' && magic[2] == 'S' && magic[3] == 'P'; - } - - /// - /// Determine if the magic bytes indicate a VPK - /// - private static bool IsVPK(byte[] magic) - { - if (magic == null || magic.Length < 4) - return false; - - return magic[0] == 0x34 && magic[1] == 0x12 && magic[2] == 0xaa && magic[3] == 0x55; - } - - /// - /// Determine if the magic bytes indicate a WAD - /// - private static bool IsWAD(byte[] magic) - { - if (magic == null || magic.Length < 4) - return false; - - return magic[0] == 'W' && magic[1] == 'A' && magic[2] == 'D' && magic[3] == '3'; - } - #endregion } }