diff --git a/BurnOutSharp.Builders/VBSP.cs b/BurnOutSharp.Builders/VBSP.cs index 5bcf1114..d65276a8 100644 --- a/BurnOutSharp.Builders/VBSP.cs +++ b/BurnOutSharp.Builders/VBSP.cs @@ -20,30 +20,10 @@ namespace BurnOutSharp.Builders public const int HL_VBSP_LUMP_ENTITIES = 0; /// - /// Idnex for the pakfile lump + /// Index for the pakfile lump /// public const int HL_VBSP_LUMP_PAKFILE = 40; - /// - /// Zip local file header signature as an integer - /// - public const int HL_VBSP_ZIP_LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50; - - /// - /// Zip file header signature as an integer - /// - public const int HL_VBSP_ZIP_FILE_HEADER_SIGNATURE = 0x02014b50; - - /// - /// Zip end of central directory record signature as an integer - /// - public const int HL_VBSP_ZIP_END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE = 0x06054b50; - - /// - /// Length of a ZIP checksum in bytes - /// - public const int HL_VBSP_ZIP_CHECKSUM_LENGTH = 0x00008000; - #endregion #region Byte Data diff --git a/BurnOutSharp.Wrappers/VBSP.cs b/BurnOutSharp.Wrappers/VBSP.cs new file mode 100644 index 00000000..8516a680 --- /dev/null +++ b/BurnOutSharp.Wrappers/VBSP.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace BurnOutSharp.Wrappers +{ + public class VBSP : WrapperBase + { + #region Pass-Through Properties + + /// + public string Signature => _file.Header.Signature; + + /// + public int Version => _file.Header.Version; + + /// + public Models.VBSP.Lump[] Lumps => _file.Header.Lumps; + + /// + public int MapRevision => _file.Header.MapRevision; + + #endregion + + #region Extension Properties + + // TODO: Figure out what extension oroperties are needed + + #endregion + + #region Instance Variables + + /// + /// Internal representation of the VBSP + /// + private Models.VBSP.File _file; + + #endregion + + #region Constructors + + /// + /// Private constructor + /// + private VBSP() { } + + /// + /// Create a VBSP from a byte array and offset + /// + /// Byte array representing the VBSP + /// Offset within the array to parse + /// A VBSP wrapper on success, null on failure + public static VBSP 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 VBSP from a Stream + /// + /// Stream representing the VBSP + /// An VBSP wrapper on success, null on failure + public static VBSP Create(Stream data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + var file = Builders.VBSP.ParseFile(data); + if (file == null) + return null; + + var wrapper = new VBSP + { + _file = file, + _dataSource = DataSource.Stream, + _streamData = data, + }; + return wrapper; + } + + #endregion + + #region Printing + + /// + public override void Print() + { + Console.WriteLine("VBSP Information:"); + Console.WriteLine("-------------------------"); + Console.WriteLine(); + + PrintHeader(); + PrintLumps(); + } + + /// + /// Print header information + /// + /// Slightly out of order due to the lumps + private void PrintHeader() + { + Console.WriteLine(" Header Information:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" Signature: {Signature}"); + Console.WriteLine($" Version: {Version}"); + Console.WriteLine($" Map revision: {MapRevision}"); + Console.WriteLine(); + } + + /// + /// Print lumps information + /// + /// Technically part of the header + private void PrintLumps() + { + Console.WriteLine(" Lumps Information:"); + Console.WriteLine(" -------------------------"); + if (Lumps == null || Lumps.Length == 0) + { + Console.WriteLine(" No lumps"); + } + else + { + for (int i = 0; i < Lumps.Length; i++) + { + var lump = Lumps[i]; + string specialLumpName = string.Empty; + switch (i) + { + case Builders.VBSP.HL_VBSP_LUMP_ENTITIES: + specialLumpName = " (entities)"; + break; + case Builders.VBSP.HL_VBSP_LUMP_PAKFILE: + specialLumpName = " (pakfile)"; + break; + } + + Console.WriteLine($" Lump {i}{specialLumpName}"); + Console.WriteLine($" Offset: {lump.Offset}"); + Console.WriteLine($" Length: {lump.Length}"); + Console.WriteLine($" Version: {lump.Version}"); + Console.WriteLine($" 4CC: {string.Join(", ", lump.FourCC)}"); + } + } + Console.WriteLine(); + } + + #endregion + + #region Extraction + + /// + /// Extract all lumps from the VBSP to an output directory + /// + /// Output directory to write to + /// True if all lumps extracted, false otherwise + public bool ExtractAllLumps(string outputDirectory) + { + // If we have no lumps + if (Lumps == null || Lumps.Length == 0) + return false; + + // Loop through and extract all lumps to the output + bool allExtracted = true; + for (int i = 0; i < Lumps.Length; i++) + { + allExtracted &= ExtractLump(i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a lump from the VBSP to an output directory by index + /// + /// Lump index to extract + /// Output directory to write to + /// True if the lump extracted, false otherwise + public bool ExtractLump(int index, string outputDirectory) + { + // If we have no lumps + if (Lumps == null || Lumps.Length == 0) + return false; + + // If the lumps index is invalid + if (index < 0 || index >= Lumps.Length) + return false; + + // Get the lump + var lump = Lumps[index]; + if (lump == null) + return false; + + // Read the data + byte[] data = ReadFromDataSource((int)lump.Offset, (int)lump.Length); + if (data == null) + return false; + + // Create the filename + string filename = $"lump_{index}.bin"; + switch (index) + { + case Builders.VBSP.HL_VBSP_LUMP_ENTITIES: + filename = "entities.ent"; + break; + case Builders.VBSP.HL_VBSP_LUMP_PAKFILE: + filename = "pakfile.zip"; + break; + } + + // 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/VBSP.cs b/BurnOutSharp/FileType/VBSP.cs new file mode 100644 index 00000000..439b1002 --- /dev/null +++ b/BurnOutSharp/FileType/VBSP.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 2 Level + /// + public class VBSP : 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 VBSP file itself fails + try + { + string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempPath); + + // Create the wrapper + Wrappers.VBSP vbsp = Wrappers.VBSP.Create(stream); + if (vbsp == null) + return null; + + // Loop through and extract all files + vbsp.ExtractAllLumps(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 c9509ec0..22b013cc 100644 --- a/BurnOutSharp/Tools/Utilities.cs +++ b/BurnOutSharp/Tools/Utilities.cs @@ -637,7 +637,7 @@ namespace BurnOutSharp.Tools case SupportedFileType.SGA: return new FileType.Valve(); case SupportedFileType.TapeArchive: return new FileType.TapeArchive(); case SupportedFileType.Textfile: return new FileType.Textfile(); - case SupportedFileType.VBSP: return new FileType.Valve(); + case SupportedFileType.VBSP: return new FileType.VBSP(); case SupportedFileType.VPK: return new FileType.VPK(); case SupportedFileType.WAD: return new FileType.Valve(); case SupportedFileType.XZ: return new FileType.XZ(); diff --git a/Test/Program.cs b/Test/Program.cs index a8b05c2e..f3440305 100644 --- a/Test/Program.cs +++ b/Test/Program.cs @@ -419,6 +419,25 @@ namespace Test pak.Print(); } + // VBSP + else if (IsVBSP(magic)) + { + // Build the archive information + Console.WriteLine("Creating VBSP deserializer"); + Console.WriteLine(); + + var vbsp = VBSP.Create(stream); + if (vbsp == null) + { + Console.WriteLine("Something went wrong parsing VBSP"); + Console.WriteLine(); + return; + } + + // Print the VBSP info to screen + vbsp.Print(); + } + // VPK else if (IsVPK(magic)) { @@ -571,6 +590,17 @@ 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 ///