From b025c7a7fa289ed7dbda7e14f4c27f5a7e125f76 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sat, 24 Dec 2022 13:49:03 -0800 Subject: [PATCH] Add VPK wrapper (nw) --- BurnOutSharp.Wrappers/VPK.cs | 323 +++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 BurnOutSharp.Wrappers/VPK.cs diff --git a/BurnOutSharp.Wrappers/VPK.cs b/BurnOutSharp.Wrappers/VPK.cs new file mode 100644 index 00000000..490a52e5 --- /dev/null +++ b/BurnOutSharp.Wrappers/VPK.cs @@ -0,0 +1,323 @@ +using System; +using System.IO; + +namespace BurnOutSharp.Wrappers +{ + public class VPK : WrapperBase + { + #region Pass-Through Properties + + #region Header + + /// + public uint Signature => _file.Header.Signature; + + /// + public uint Version => _file.Header.Version; + + /// + public uint DirectoryLength => _file.Header.DirectoryLength; + + #endregion + + #region Extended Header + + /// + public uint? Dummy0 => _file.ExtendedHeader?.Dummy0; + + /// + public uint? ArchiveHashLength => _file.ExtendedHeader?.ArchiveHashLength; + + /// + public uint? ExtraLength => _file.ExtendedHeader?.ExtraLength; + + /// + public uint? Dummy1 => _file.ExtendedHeader?.Dummy1; + + #endregion + + #region Archive Hashes + + /// + public Models.VPK.ArchiveHash[] ArchiveHashes => _file.ArchiveHashes; + + #endregion + + #region Directory Items + + /// + public Models.VPK.DirectoryItem[] DirectoryItems => _file.DirectoryItems; + + #endregion + + #endregion + + #region Extension Properties + + // TODO: Determine what extension properties are needed + + #endregion + + #region Instance Variables + + /// + /// Internal representation of the executable + /// + private Models.VPK.File _file; + + #endregion + + #region Constructors + + /// + /// Private constructor + /// + private VPK() { } + + /// + /// Create an VPK from a byte array and offset + /// + /// Byte array representing the executable + /// Offset within the array to parse + /// An VPK wrapper on success, null on failure + public static VPK 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 anVPK from a Stream + /// + /// Stream representing the executable + /// An VPK wrapper on success, null on failure + public static VPK Create(Stream data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + var file = Builders.VPK.ParseFile(data); + if (file == null) + return null; + + var wrapper = new VPK + { + _file = file, + _dataSource = DataSource.Stream, + _streamData = data, + }; + return wrapper; + } + + #endregion + + #region Printing + + /// + public override void Print() + { + Console.WriteLine("VPK Information:"); + Console.WriteLine("-------------------------"); + Console.WriteLine(); + + PrintHeader(); + PrintExtendedHeader(); + PrintArchiveHashes(); + PrintDirectoryItems(); + } + + /// + /// Print header information + /// + private void PrintHeader() + { + Console.WriteLine(" Header Information:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" Signature: {Signature}"); + Console.WriteLine($" Version: {Version}"); + Console.WriteLine($" Directory length: {DirectoryLength}"); + Console.WriteLine(); + } + + /// + /// Print extended header information + /// + private void PrintExtendedHeader() + { + Console.WriteLine(" Extended Header Information:"); + Console.WriteLine(" -------------------------"); + if (_file.ExtendedHeader == null) + { + Console.WriteLine(" No extended header"); + } + else + { + Console.WriteLine($" Dummy 0: {Dummy0}"); + Console.WriteLine($" Archive hash length: {ArchiveHashLength}"); + Console.WriteLine($" Extra length: {ExtraLength}"); + Console.WriteLine($" Dummy 1: {Dummy1}"); + Console.WriteLine(); + } + } + + /// + /// Print archive hashes information + /// + private void PrintArchiveHashes() + { + Console.WriteLine(" Archive Hashes Information:"); + Console.WriteLine(" -------------------------"); + if (ArchiveHashes == null || ArchiveHashes.Length == 0) + { + Console.WriteLine(" No archive hashes"); + } + else + { + for (int i = 0; i < ArchiveHashes.Length; i++) + { + var archiveHash = ArchiveHashes[i]; + Console.WriteLine($" Archive Hash {i}"); + Console.WriteLine($" Archive index: {archiveHash.ArchiveIndex}"); + Console.WriteLine($" Archive offset: {archiveHash.ArchiveOffset}"); + Console.WriteLine($" Length: {archiveHash.Length}"); + Console.WriteLine($" Hash: {BitConverter.ToString(archiveHash.Hash).Replace("-", string.Empty)}"); + } + } + 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($" Extension: {directoryItem.Extension}"); + Console.WriteLine($" Path: {directoryItem.Path}"); + Console.WriteLine($" Name: {directoryItem.Name}"); + PrintDirectoryEntry(directoryItem.DirectoryEntry); + // TODO: Print out preload data? + } + } + Console.WriteLine(); + } + + /// + /// Print directory entry information + /// + private void PrintDirectoryEntry(Models.VPK.DirectoryEntry directoryEntry) + { + if (directoryEntry == null) + { + Console.WriteLine(" Directory entry: [NULL]"); + } + else + { + Console.WriteLine($" Directory entry CRC: {directoryEntry.CRC}"); + Console.WriteLine($" Directory entry preload bytes: {directoryEntry.PreloadBytes}"); + Console.WriteLine($" Directory entry archive index: {directoryEntry.ArchiveIndex}"); + Console.WriteLine($" Directory entry entry offset: {directoryEntry.EntryOffset}"); + Console.WriteLine($" Directory entry entry length: {directoryEntry.EntryLength}"); + Console.WriteLine($" Directory entry dummy 0: {directoryEntry.Dummy0}"); + } + } + + #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 (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 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 (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?.DirectoryEntry == null) + return false; + + // Create the filename + string filename = $"{directoryItem.Name}.{directoryItem.Extension}"; + if (!string.IsNullOrEmpty(directoryItem.Path)) + filename = Path.Combine(directoryItem.Path, filename); + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Ensure the output directory is created + Directory.CreateDirectory(outputDirectory); + + // Create the full output path + filename = Path.Combine(outputDirectory, filename); + + // Read the directory item bytes + int offset = (int)directoryItem.DirectoryEntry.EntryOffset; + int length = (int)directoryItem.DirectoryEntry.EntryLength; + byte[] data = ReadFromDataSource(offset, length); + + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + + return true; + } + + #endregion + } +} \ No newline at end of file