diff --git a/BurnOutSharp.Builders/PAK.cs b/BurnOutSharp.Builders/PAK.cs index 0d86799e..d8c7d479 100644 --- a/BurnOutSharp.Builders/PAK.cs +++ b/BurnOutSharp.Builders/PAK.cs @@ -104,6 +104,9 @@ namespace BurnOutSharp.Builders byte[] signature = data.ReadBytes(4); header.Signature = Encoding.ASCII.GetString(signature); + if (header.Signature != "PACK") + return null; + header.DirectoryOffset = data.ReadUInt32(); header.DirectoryLength = data.ReadUInt32(); @@ -121,7 +124,7 @@ namespace BurnOutSharp.Builders DirectoryItem directoryItem = new DirectoryItem(); byte[] itemName = data.ReadBytes(56); - directoryItem.ItemName = Encoding.ASCII.GetString(itemName); + directoryItem.ItemName = Encoding.ASCII.GetString(itemName).TrimEnd('\0'); directoryItem.ItemOffset = data.ReadUInt32(); directoryItem.ItemLength = data.ReadUInt32(); diff --git a/BurnOutSharp.Wrappers/PAK.cs b/BurnOutSharp.Wrappers/PAK.cs new file mode 100644 index 00000000..e31dc1d5 --- /dev/null +++ b/BurnOutSharp.Wrappers/PAK.cs @@ -0,0 +1,233 @@ +using System; +using System.IO; + +namespace BurnOutSharp.Wrappers +{ + public class PAK : WrapperBase + { + #region Pass-Through Properties + + #region Header + + /// + public string Signature => _file.Header.Signature; + + /// + public uint DirectoryOffset => _file.Header.DirectoryOffset; + + /// + public uint DirectoryLength => _file.Header.DirectoryLength; + + #endregion + + #region Directory Items + + /// + public Models.PAK.DirectoryItem[] DirectoryItems => _file.DirectoryItems; + + #endregion + + #endregion + + #region Extension Properties + + // TODO: Figure out what extensions are needed + + #endregion + + #region Instance Variables + + /// + /// Internal representation of the PAK + /// + private Models.PAK.File _file; + + #endregion + + #region Constructors + + /// + /// Private constructor + /// + private PAK() { } + + /// + /// 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 + public static PAK 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 PAK from a Stream + /// + /// Stream representing the PAK + /// A PAK wrapper on success, null on failure + public static PAK Create(Stream data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + var file = Builders.PAK.ParseFile(data); + if (file == null) + return null; + + var wrapper = new PAK + { + _file = file, + _dataSource = DataSource.Stream, + _streamData = data, + }; + return wrapper; + } + + #endregion + + #region Printing + + /// + public override void Print() + { + Console.WriteLine("PAK Information:"); + Console.WriteLine("-------------------------"); + Console.WriteLine(); + + PrintHeader(); + PrintDirectoryItems(); + } + + /// + /// Print header information + /// + private void PrintHeader() + { + Console.WriteLine(" Header Information:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" Signature: {Signature}"); + Console.WriteLine($" Directory offset: {DirectoryOffset}"); + Console.WriteLine($" Directory length: {DirectoryLength}"); + 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($" Item name: {directoryItem.ItemName}"); + Console.WriteLine($" Item offset: {directoryItem.ItemOffset}"); + Console.WriteLine($" Item length: {directoryItem.ItemLength}"); + } + } + Console.WriteLine(); + } + + #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 (DirectoryItems == null || DirectoryItems.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < 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 (DirectoryItems == null || DirectoryItems.Length == 0) + return false; + + // If the directory item index is invalid + if (index < 0 || index >= DirectoryItems.Length) + return false; + + // Get the directory item + var directoryItem = DirectoryItems[index]; + if (directoryItem == null) + return false; + + // Read the item data + byte[] data = ReadFromDataSource((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength); + + // Create the filename + string filename = directoryItem.ItemName; + + // 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/GCF.cs b/BurnOutSharp/FileType/GCF.cs index 54f0ee27..0d1e270e 100644 --- a/BurnOutSharp/FileType/GCF.cs +++ b/BurnOutSharp/FileType/GCF.cs @@ -26,7 +26,7 @@ namespace BurnOutSharp.FileType /// public ConcurrentDictionary> Scan(Scanner scanner, Stream stream, string file) { - // If the BSP file itself fails + // If the GCF file itself fails try { string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); diff --git a/BurnOutSharp/FileType/PAK.cs b/BurnOutSharp/FileType/PAK.cs new file mode 100644 index 00000000..4073f9fe --- /dev/null +++ b/BurnOutSharp/FileType/PAK.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 +{ + /// + /// Half-Life Package File + /// + public class PAK : 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 PAK file itself fails + try + { + string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempPath); + + // Create the wrapper + Wrappers.PAK pak = Wrappers.PAK.Create(stream); + if (pak == null) + return null; + + // Loop through and extract all files + pak.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 71702b42..c9509ec0 100644 --- a/BurnOutSharp/Tools/Utilities.cs +++ b/BurnOutSharp/Tools/Utilities.cs @@ -628,7 +628,7 @@ namespace BurnOutSharp.Tools case SupportedFileType.MicrosoftCAB: return new FileType.MicrosoftCAB(); case SupportedFileType.MPQ: return new FileType.MPQ(); case SupportedFileType.MSI: return new FileType.MSI(); - case SupportedFileType.PAK: return new FileType.PKZIP(); + case SupportedFileType.PAK: return new FileType.PAK(); case SupportedFileType.PKZIP: return new FileType.PKZIP(); case SupportedFileType.PLJ: return new FileType.PLJ(); case SupportedFileType.RAR: return new FileType.RAR(); diff --git a/Test/Program.cs b/Test/Program.cs index e0aa6d30..a8b05c2e 100644 --- a/Test/Program.cs +++ b/Test/Program.cs @@ -400,6 +400,25 @@ namespace Test ncf.Print(); } + // PAK + else if (IsPAK(magic)) + { + // Build the archive information + Console.WriteLine("Creating PAK deserializer"); + Console.WriteLine(); + + var pak = PAK.Create(stream); + if (pak == null) + { + Console.WriteLine("Something went wrong parsing PAK"); + Console.WriteLine(); + return; + } + + // Print the PAK info to screen + pak.Print(); + } + // VPK else if (IsVPK(magic)) { @@ -530,6 +549,17 @@ 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 ///