From 751669eb64e1dd2ec6b7cd49e0f68f4e1d799029 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 8 Sep 2023 23:14:44 -0400 Subject: [PATCH] Add and convert VPK --- Bytes/VPK.Deserializer.cs | 27 ++++ Streams/VPK.Deserializer.cs | 289 ++++++++++++++++++++++++++++++++++++ Streams/VPK.Serializer.cs | 15 ++ 3 files changed, 331 insertions(+) create mode 100644 Bytes/VPK.Deserializer.cs create mode 100644 Streams/VPK.Deserializer.cs create mode 100644 Streams/VPK.Serializer.cs diff --git a/Bytes/VPK.Deserializer.cs b/Bytes/VPK.Deserializer.cs new file mode 100644 index 00000000..d431f4a1 --- /dev/null +++ b/Bytes/VPK.Deserializer.cs @@ -0,0 +1,27 @@ +using System.IO; + +namespace SabreTools.Serialization.Bytes +{ + public partial class VPK : IByteSerializer + { + /// +#if NET48 + public Models.VPK.File Deserialize(byte[] data, int offset) +#else + public Models.VPK.File? Deserialize(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 parse that + MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); + return new Streams.VPK().Deserialize(dataStream); + } + } +} \ No newline at end of file diff --git a/Streams/VPK.Deserializer.cs b/Streams/VPK.Deserializer.cs new file mode 100644 index 00000000..4f11f0dd --- /dev/null +++ b/Streams/VPK.Deserializer.cs @@ -0,0 +1,289 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.VPK; +using static SabreTools.Models.VPK.Constants; + +namespace SabreTools.Serialization.Streams +{ + public partial class VPK : IStreamSerializer + { + /// +#if NET48 + public Models.VPK.File Deserialize(Stream data) +#else + public Models.VPK.File? Deserialize(Stream? data) +#endif + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + long initialOffset = data.Position; + + // Create a new Valve Package to fill + var file = new Models.VPK.File(); + + #region Header + + // Try to parse the header + // The original version had no signature. + var header = ParseHeader(data); + + // Set the package header + file.Header = header; + + #endregion + + #region Extended Header + + if (header?.Version == 2) + { + // Try to parse the extended header + var extendedHeader = ParseExtendedHeader(data); + if (extendedHeader == null) + return null; + + // Set the package extended header + file.ExtendedHeader = extendedHeader; + } + + #endregion + + #region Directory Items + + // Create the directory items tree + var directoryItems = ParseDirectoryItemTree(data); + + // Set the directory items + file.DirectoryItems = directoryItems; + + #endregion + + #region Archive Hashes + + if (header?.Version == 2 && file.ExtendedHeader != null && file.ExtendedHeader.ArchiveHashLength > 0) + { + // Create the archive hashes list + var archiveHashes = new List(); + + // Cache the current offset + initialOffset = data.Position; + + // Try to parse the directory items + while (data.Position < initialOffset + file.ExtendedHeader.ArchiveHashLength) + { + var archiveHash = ParseArchiveHash(data); + archiveHashes.Add(archiveHash); + } + + file.ArchiveHashes = archiveHashes.ToArray(); + } + + #endregion + + return file; + } + + /// + /// Parse a Stream into a Valve Package header + /// + /// Stream to parse + /// Filled Valve Package header on success, null on error + private static Header ParseHeader(Stream data) + { + // TODO: Use marshalling here instead of building + Header header = new Header(); + + header.Signature = data.ReadUInt32(); + if (header.Signature != SignatureUInt32) + return null; + + header.Version = data.ReadUInt32(); + if (header.Version > 2) + return null; + + header.DirectoryLength = data.ReadUInt32(); + + return header; + } + + /// + /// Parse a Stream into a Valve Package extended header + /// + /// Stream to parse + /// Filled Valve Package extended header on success, null on error + private static ExtendedHeader ParseExtendedHeader(Stream data) + { + // TODO: Use marshalling here instead of building + ExtendedHeader extendedHeader = new ExtendedHeader(); + + extendedHeader.Dummy0 = data.ReadUInt32(); + extendedHeader.ArchiveHashLength = data.ReadUInt32(); + extendedHeader.ExtraLength = data.ReadUInt32(); + extendedHeader.Dummy1 = data.ReadUInt32(); + + return extendedHeader; + } + + /// + /// Parse a Stream into a Valve Package archive hash + /// + /// Stream to parse + /// Filled Valve Package archive hash on success, null on error + private static ArchiveHash ParseArchiveHash(Stream data) + { + // TODO: Use marshalling here instead of building + ArchiveHash archiveHash = new ArchiveHash(); + + archiveHash.ArchiveIndex = data.ReadUInt32(); + archiveHash.ArchiveOffset = data.ReadUInt32(); + archiveHash.Length = data.ReadUInt32(); + archiveHash.Hash = data.ReadBytes(0x10); + + return archiveHash; + } + + /// + /// Parse a Stream into a Valve Package directory item tree + /// + /// Stream to parse + /// Filled Valve Package directory item tree on success, null on error + private static DirectoryItem[] ParseDirectoryItemTree(Stream data) + { + // Create the directory items list + var directoryItems = new List(); + + while (true) + { + // Get the extension + string extensionString = data.ReadString(Encoding.ASCII); + if (string.IsNullOrEmpty(extensionString)) + break; + + // Sanitize the extension + for (int i = 0; i < 0x20; i++) + { + extensionString = extensionString.Replace($"{(char)i}", string.Empty); + } + + while (true) + { + // Get the path + string pathString = data.ReadString(Encoding.ASCII); + if (string.IsNullOrEmpty(pathString)) + break; + + // Sanitize the path + for (int i = 0; i < 0x20; i++) + { + pathString = pathString.Replace($"{(char)i}", string.Empty); + } + + while (true) + { + // Get the name + string nameString = data.ReadString(Encoding.ASCII); + if (string.IsNullOrEmpty(nameString)) + break; + + // Sanitize the name + for (int i = 0; i < 0x20; i++) + { + nameString = nameString.Replace($"{(char)i}", string.Empty); + } + + // Get the directory item + var directoryItem = ParseDirectoryItem(data, extensionString, pathString, nameString); + + // Add the directory item + directoryItems.Add(directoryItem); + } + } + } + + return directoryItems.ToArray(); + } + + /// + /// Parse a Stream into a Valve Package directory item + /// + /// Stream to parse + /// Filled Valve Package directory item on success, null on error + private static DirectoryItem ParseDirectoryItem(Stream data, string extension, string path, string name) + { + DirectoryItem directoryItem = new DirectoryItem(); + + directoryItem.Extension = extension; + directoryItem.Path = path; + directoryItem.Name = name; + + // Get the directory entry + var directoryEntry = ParseDirectoryEntry(data); + + // Set the directory entry + directoryItem.DirectoryEntry = directoryEntry; + + // Get the preload data pointer + long preloadDataPointer = -1; int preloadDataLength = -1; + if (directoryEntry.ArchiveIndex == HL_VPK_NO_ARCHIVE && directoryEntry.EntryLength > 0) + { + preloadDataPointer = directoryEntry.EntryOffset; + preloadDataLength = (int)directoryEntry.EntryLength; + } + else if (directoryEntry.PreloadBytes > 0) + { + preloadDataPointer = data.Position; + preloadDataLength = directoryEntry.PreloadBytes; + } + + // If we had a valid preload data pointer + byte[] preloadData = null; + if (preloadDataPointer >= 0 && preloadDataLength > 0) + { + // Cache the current offset + long initialOffset = data.Position; + + // Seek to the preload data offset + data.Seek(preloadDataPointer, SeekOrigin.Begin); + + // Read the preload data + preloadData = data.ReadBytes(preloadDataLength); + + // Seek back to the original offset + data.Seek(initialOffset, SeekOrigin.Begin); + } + + // Set the preload data + directoryItem.PreloadData = preloadData; + + return directoryItem; + } + + /// + /// Parse a Stream into a Valve Package directory entry + /// + /// Stream to parse + /// Filled Valve Package directory entry on success, null on error + private static DirectoryEntry ParseDirectoryEntry(Stream data) + { + // TODO: Use marshalling here instead of building + DirectoryEntry directoryEntry = new DirectoryEntry(); + + directoryEntry.CRC = data.ReadUInt32(); + directoryEntry.PreloadBytes = data.ReadUInt16(); + directoryEntry.ArchiveIndex = data.ReadUInt16(); + directoryEntry.EntryOffset = data.ReadUInt32(); + directoryEntry.EntryLength = data.ReadUInt32(); + directoryEntry.Dummy0 = data.ReadUInt16(); + + return directoryEntry; + } + } +} \ No newline at end of file diff --git a/Streams/VPK.Serializer.cs b/Streams/VPK.Serializer.cs new file mode 100644 index 00000000..979fb3e6 --- /dev/null +++ b/Streams/VPK.Serializer.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; + +namespace SabreTools.Serialization.Streams +{ + public partial class VPK : IStreamSerializer + { + /// +#if NET48 + public Stream Serialize(Models.VPK.File obj) => throw new NotImplementedException(); +#else + public Stream? Serialize(Models.VPK.File? obj) => throw new NotImplementedException(); +#endif + } +} \ No newline at end of file