From 81902455ff43b35cea63c2f22281d0675be017fd Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sat, 24 Dec 2022 15:25:56 -0800 Subject: [PATCH] Fix VPK extraction --- BurnOutSharp.Builders/VPK.cs | 40 ++++++++--- BurnOutSharp.Wrappers/VPK.cs | 133 +++++++++++++++++++++++++++++++---- 2 files changed, 150 insertions(+), 23 deletions(-) diff --git a/BurnOutSharp.Builders/VPK.cs b/BurnOutSharp.Builders/VPK.cs index 7914cdd2..7b14a83f 100644 --- a/BurnOutSharp.Builders/VPK.cs +++ b/BurnOutSharp.Builders/VPK.cs @@ -101,6 +101,16 @@ namespace BurnOutSharp.Builders #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) @@ -123,16 +133,6 @@ namespace BurnOutSharp.Builders #endregion - #region Directory Items - - // Create the directory items tree - var directoryItems = ParseDirectoryItemTree(data); - - // Set the directory items - file.DirectoryItems = directoryItems; - - #endregion - return file; } @@ -212,6 +212,12 @@ namespace BurnOutSharp.Builders 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 @@ -219,6 +225,12 @@ namespace BurnOutSharp.Builders 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 @@ -226,9 +238,15 @@ namespace BurnOutSharp.Builders 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); } diff --git a/BurnOutSharp.Wrappers/VPK.cs b/BurnOutSharp.Wrappers/VPK.cs index 490a52e5..4719bbf1 100644 --- a/BurnOutSharp.Wrappers/VPK.cs +++ b/BurnOutSharp.Wrappers/VPK.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Linq; +using BurnOutSharp.Utilities; namespace BurnOutSharp.Wrappers { @@ -54,7 +56,51 @@ namespace BurnOutSharp.Wrappers #region Extension Properties - // TODO: Determine what extension properties are needed + /// + /// Array of archive filenames attached to the given VPK + /// + public string[] ArchiveFilenames + { + 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('.'); + string fileName = Path.Combine(Path.GetDirectoryName(fs.Name), 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 = DirectoryItems + .Select(di => di.DirectoryEntry) + .Select(de => de.ArchiveIndex) + .Where(ai => ai != Builders.VPK.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 @@ -65,6 +111,11 @@ namespace BurnOutSharp.Wrappers /// private Models.VPK.File _file; + /// + /// Array of archive filenames attached to the given VPK + /// + private string[] _archiveFilenames = null; + #endregion #region Constructors @@ -289,30 +340,88 @@ namespace BurnOutSharp.Wrappers if (directoryItem?.DirectoryEntry == null) return false; + // If we have an item with no archive + byte[] data; + if (directoryItem.DirectoryEntry.ArchiveIndex == Builders.VPK.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 + Stream archiveStream = null; + 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 (directoryItem.PreloadData != null) + data = directoryItem.PreloadData.Concat(data).ToArray(); + } + // Create the filename string filename = $"{directoryItem.Name}.{directoryItem.Extension}"; - if (!string.IsNullOrEmpty(directoryItem.Path)) + if (!string.IsNullOrWhiteSpace(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); + // Ensure the output directory is created + Directory.CreateDirectory(Path.GetDirectoryName(filename)); - // Open the output file for writing - using (Stream fs = File.OpenWrite(filename)) + // Try to write the data + try { - fs.Write(data, 0, data.Length); + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + } + catch + { + return false; } return true;