From 2416a035c722c4bfba29e7e5b13a4a5dff4f7803 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 3 Jan 2023 09:28:16 -0800 Subject: [PATCH] Add Quantum archive models/builder/wrapper --- BurnOutSharp.Builders/Quantum.cs | 176 ++++++++++++++ BurnOutSharp.Models/Quantum/Archive.cs | 21 ++ BurnOutSharp.Models/Quantum/Constants.cs | 11 + .../Quantum/FileDescriptor.cs | 22 +- .../Quantum/Archive.cs => Quantum/Header.cs} | 15 +- BurnOutSharp.Wrappers/Quantum.cs | 214 ++++++++++++++++++ BurnOutSharp/Enums.cs | 5 + BurnOutSharp/Tools/Utilities.cs | 25 +- Test/Program.cs | 19 ++ 9 files changed, 488 insertions(+), 20 deletions(-) create mode 100644 BurnOutSharp.Builders/Quantum.cs create mode 100644 BurnOutSharp.Models/Quantum/Archive.cs create mode 100644 BurnOutSharp.Models/Quantum/Constants.cs rename BurnOutSharp.Models/{Compression => }/Quantum/FileDescriptor.cs (62%) rename BurnOutSharp.Models/{Compression/Quantum/Archive.cs => Quantum/Header.cs} (70%) create mode 100644 BurnOutSharp.Wrappers/Quantum.cs diff --git a/BurnOutSharp.Builders/Quantum.cs b/BurnOutSharp.Builders/Quantum.cs new file mode 100644 index 00000000..9faed8d1 --- /dev/null +++ b/BurnOutSharp.Builders/Quantum.cs @@ -0,0 +1,176 @@ +using System.IO; +using System.Text; +using BurnOutSharp.Models.Quantum; +using BurnOutSharp.Utilities; +using static BurnOutSharp.Models.Quantum.Constants; + +namespace BurnOutSharp.Builders +{ + public class Quantum + { + #region Byte Data + + /// + /// Parse a byte array into a Quantum archive + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled archive on success, null on error + public static Archive ParseArchive(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 parse that + MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); + return ParseArchive(dataStream); + } + + #endregion + + #region Stream Data + + /// + /// Parse a Stream into a Quantum archive + /// + /// Stream to parse + /// Filled archive on success, null on error + public static Archive ParseArchive(Stream data) + { + // 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 + int initialOffset = (int)data.Position; + + // Create a new archive to fill + var archive = new Archive(); + + #region Header + + // Try to parse the header + var header = ParseHeader(data); + if (header == null) + return null; + + // Set the archive header + archive.Header = header; + + #endregion + + #region File List + + // If we have any files + if (header.FileCount > 0) + { + var fileDescriptors = new FileDescriptor[header.FileCount]; + + // Read all entries in turn + for (int i = 0; i < header.FileCount; i++) + { + var file = ParseFileDescriptor(data); + if (file == null) + return null; + + fileDescriptors[i] = file; + } + + // Set the file list + archive.FileList = fileDescriptors; + } + + #endregion + + return archive; + } + + /// + /// Parse a Stream into a header + /// + /// Stream to parse + /// Filled header on success, null on error + private static Header ParseHeader(Stream data) + { + // TODO: Use marshalling here instead of building + Header header = new Header(); + + byte[] signature = data.ReadBytes(2); + header.Signature = Encoding.ASCII.GetString(signature); + if (header.Signature != SignatureString) + return null; + + header.MajorVersion = data.ReadByteValue(); + header.MinorVersion = data.ReadByteValue(); + header.FileCount = data.ReadUInt16(); + header.TableSize = data.ReadByteValue(); + header.CompressionFlags = data.ReadByteValue(); + + return header; + } + + /// + /// Parse a Stream into a file descriptor + /// + /// Stream to parse + /// Filled file descriptor on success, null on error + private static FileDescriptor ParseFileDescriptor(Stream data) + { + // TODO: Use marshalling here instead of building + FileDescriptor fileDescriptor = new FileDescriptor(); + + fileDescriptor.FileNameSize = ReadVariableLength(data); + if (fileDescriptor.FileNameSize > 0) + { + byte[] fileName = data.ReadBytes(fileDescriptor.FileNameSize); + fileDescriptor.FileName = Encoding.ASCII.GetString(fileName); + } + + fileDescriptor.CommentFieldSize = ReadVariableLength(data); + if (fileDescriptor.CommentFieldSize > 0) + { + byte[] commentField = data.ReadBytes(fileDescriptor.CommentFieldSize); + fileDescriptor.CommentField = Encoding.ASCII.GetString(commentField); + } + + fileDescriptor.ExpandedFileSize = data.ReadUInt32(); + fileDescriptor.FileTime = data.ReadUInt16(); + fileDescriptor.FileDate = data.ReadUInt16(); + + return fileDescriptor; + } + + /// + /// Parse a Stream into a variable-length size prefix + /// + /// Stream to parse + /// Variable-length size prefix + /// + /// Strings are prefixed with their length. If the length is less than 128 + /// then it is stored directly in one byte. If it is greater than 127 then + /// the high bit of the first byte is set to 1 and the remaining fifteen bits + /// contain the actual length in big-endian format. + /// + private static int ReadVariableLength(Stream data) + { + byte b0 = data.ReadByteValue(); + if (b0 < 0x7F) + return b0; + + b0 &= 0x7F; + byte b1 = data.ReadByteValue(); + return (b0 << 8) | b1; + } + + #endregion + } +} diff --git a/BurnOutSharp.Models/Quantum/Archive.cs b/BurnOutSharp.Models/Quantum/Archive.cs new file mode 100644 index 00000000..4f6722a6 --- /dev/null +++ b/BurnOutSharp.Models/Quantum/Archive.cs @@ -0,0 +1,21 @@ +namespace BurnOutSharp.Models.Quantum +{ + /// + /// Quantum archive file structure + /// + /// + public class Archive + { + /// + /// Quantum header + /// + public Header Header { get; set; } + + /// + /// This is immediately followed by the list of files + /// + public FileDescriptor[] FileList { get; set; } + + // Immediately following the list of files is the compressed data. + } +} \ No newline at end of file diff --git a/BurnOutSharp.Models/Quantum/Constants.cs b/BurnOutSharp.Models/Quantum/Constants.cs new file mode 100644 index 00000000..274a9469 --- /dev/null +++ b/BurnOutSharp.Models/Quantum/Constants.cs @@ -0,0 +1,11 @@ +namespace BurnOutSharp.Models.Quantum +{ + public static class Constants + { + public static readonly byte[] SignatureBytes = new byte[] { 0x44, 0x53 }; + + public const string SignatureString = "DS"; + + public const ushort SignatureUInt16 = 0x5344; + } +} \ No newline at end of file diff --git a/BurnOutSharp.Models/Compression/Quantum/FileDescriptor.cs b/BurnOutSharp.Models/Quantum/FileDescriptor.cs similarity index 62% rename from BurnOutSharp.Models/Compression/Quantum/FileDescriptor.cs rename to BurnOutSharp.Models/Quantum/FileDescriptor.cs index 00d8ed9f..7b4a79a8 100644 --- a/BurnOutSharp.Models/Compression/Quantum/FileDescriptor.cs +++ b/BurnOutSharp.Models/Quantum/FileDescriptor.cs @@ -1,18 +1,26 @@ -namespace BurnOutSharp.Models.Compression.Quantum +namespace BurnOutSharp.Models.Quantum { - /// - /// Strings are prefixed with their length. If the length is less than - /// 128 then it is stored directly in one byte. If it is greater than 127 - /// then the high bit of the first byte is set to 1 and the remaining - /// fifteen bits contain the actual length in big-endian format. - /// + /// + /// Quantum archive file descriptor + /// + /// public class FileDescriptor { + /// + /// Length of file name + /// + public int FileNameSize; + /// /// File name, variable length string, not zero-terminated /// public string FileName; + /// + /// Length of comment field + /// + public int CommentFieldSize; + /// /// Comment field, variable length string, not zero-terminated /// diff --git a/BurnOutSharp.Models/Compression/Quantum/Archive.cs b/BurnOutSharp.Models/Quantum/Header.cs similarity index 70% rename from BurnOutSharp.Models/Compression/Quantum/Archive.cs rename to BurnOutSharp.Models/Quantum/Header.cs index fd01dd76..22252b0b 100644 --- a/BurnOutSharp.Models/Compression/Quantum/Archive.cs +++ b/BurnOutSharp.Models/Quantum/Header.cs @@ -1,18 +1,18 @@ using System.Runtime.InteropServices; -namespace BurnOutSharp.Models.Compression.Quantum +namespace BurnOutSharp.Models.Quantum { /// - /// Quantum archive file structure + /// Quantum archive file header /// /// [StructLayout(LayoutKind.Sequential)] - public class Archive + public class Header { /// /// Quantum signature: 0x44 0x53 /// - public ushort Signature; + public string Signature; /// /// Quantum major version number @@ -38,12 +38,5 @@ namespace BurnOutSharp.Models.Compression.Quantum /// Compression flags /// public byte CompressionFlags; - - /// - /// This is immediately followed by the list of files - /// - public FileDescriptor[] FileList; - - // Immediately following the list of files is the compressed data. } } \ No newline at end of file diff --git a/BurnOutSharp.Wrappers/Quantum.cs b/BurnOutSharp.Wrappers/Quantum.cs new file mode 100644 index 00000000..3fe48376 --- /dev/null +++ b/BurnOutSharp.Wrappers/Quantum.cs @@ -0,0 +1,214 @@ +using System; +using System.IO; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate; + +namespace BurnOutSharp.Wrappers +{ + public class Quantum : WrapperBase + { + #region Pass-Through Properties + + #region Header + + /// + public string Signature => _archive.Header.Signature; + + /// + public byte MajorVersion => _archive.Header.MajorVersion; + + /// + public byte MinorVersion => _archive.Header.MinorVersion; + + /// + public ushort FileCount => _archive.Header.FileCount; + + /// + public byte TableSize => _archive.Header.TableSize; + + /// + public byte CompressionFlags => _archive.Header.CompressionFlags; + + #endregion + + #region Files + + /// + public Models.Quantum.FileDescriptor[] FileList => _archive.FileList; + + #endregion + + #endregion + + #region Instance Variables + + /// + /// Internal representation of the archive + /// + private Models.Quantum.Archive _archive; + + #endregion + + #region Constructors + + /// + /// Private constructor + /// + private Quantum() { } + + /// + /// Create a Quantum archive from a byte array and offset + /// + /// Byte array representing the archive + /// Offset within the array to parse + /// A Quantum archive wrapper on success, null on failure + public static Quantum 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 Quantum archive from a Stream + /// + /// Stream representing the archive + /// A Quantum archive wrapper on success, null on failure + public static Quantum Create(Stream data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + var archive = Builders.Quantum.ParseArchive(data); + if (archive == null) + return null; + + var wrapper = new Quantum + { + _archive = archive, + _dataSource = DataSource.Stream, + _streamData = data, + }; + return wrapper; + } + + #endregion + + #region Data + + /// + /// Extract all files from the Quantum archive 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 files + if (FileList == null || FileList.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < FileList.Length; i++) + { + allExtracted &= ExtractFile(i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a file from the Quantum archive 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 files + if (FileCount == 0 || FileList == null || FileList.Length == 0) + return false; + + // If we have an invalid index + if (index < 0 || index >= FileList.Length) + return false; + + // Get the file information + var file = FileList[index]; + + // TODO: Is it even possible to extract a single file? + return false; + } + + #endregion + + #region Printing + + /// + public override void Print() + { + Console.WriteLine("Quantum Information:"); + Console.WriteLine("-------------------------"); + Console.WriteLine(); + + PrintHeader(); + PrintFileList(); + } + + /// + /// Print header information + /// + private void PrintHeader() + { + Console.WriteLine(" Header Information:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" Signature: {Signature}"); + Console.WriteLine($" Major version: {MajorVersion}"); + Console.WriteLine($" Minor version: {MinorVersion}"); + Console.WriteLine($" File count: {FileCount}"); + Console.WriteLine($" Table size: {TableSize}"); + Console.WriteLine($" Compression flags: {CompressionFlags}"); + Console.WriteLine(); + } + + /// + /// Print file list information + /// + private void PrintFileList() + { + Console.WriteLine(" File List Information:"); + Console.WriteLine(" -------------------------"); + if (FileCount == 0 || FileList == null || FileList.Length == 0) + { + Console.WriteLine(" No file list items"); + } + else + { + for (int i = 0; i < FileList.Length; i++) + { + var fileDescriptor = FileList[i]; + Console.WriteLine($" File Descriptor {i}"); + Console.WriteLine($" File name size = {fileDescriptor.FileNameSize}"); + Console.WriteLine($" File name = {fileDescriptor.FileName ?? "[NULL]"}"); + Console.WriteLine($" Comment field size = {fileDescriptor.CommentFieldSize}"); + Console.WriteLine($" Comment field = {fileDescriptor.CommentField ?? "[NULL]"}"); + Console.WriteLine($" Expanded file size = {fileDescriptor.ExpandedFileSize}"); + Console.WriteLine($" File time = {fileDescriptor.FileTime}"); + Console.WriteLine($" File date = {fileDescriptor.FileDate}"); + } + } + Console.WriteLine(); + } + + #endregion + } +} \ No newline at end of file diff --git a/BurnOutSharp/Enums.cs b/BurnOutSharp/Enums.cs index f7551075..bde37a8a 100644 --- a/BurnOutSharp/Enums.cs +++ b/BurnOutSharp/Enums.cs @@ -95,6 +95,11 @@ /// PLJ, + /// + /// Quantum archive + /// + Quantum, + /// /// RAR archive /// diff --git a/BurnOutSharp/Tools/Utilities.cs b/BurnOutSharp/Tools/Utilities.cs index f53ee454..f288b39a 100644 --- a/BurnOutSharp/Tools/Utilities.cs +++ b/BurnOutSharp/Tools/Utilities.cs @@ -186,6 +186,13 @@ namespace BurnOutSharp.Tools #endregion + #region Quantum + + if (magic.StartsWith(new byte?[] { 0x44, 0x53 })) + return SupportedFileType.Quantum; + + #endregion + #region RAR // RAR archive version 1.50 onwards @@ -410,8 +417,10 @@ namespace BurnOutSharp.Tools #region PAK - if (extension.Equals("pak", StringComparison.OrdinalIgnoreCase)) - return SupportedFileType.PAK; + // No extensions registered for PAK + // Both PAK and Quantum share one extension + // if (extension.Equals("pak", StringComparison.OrdinalIgnoreCase)) + // return SupportedFileType.PAK; #endregion @@ -511,6 +520,17 @@ namespace BurnOutSharp.Tools #endregion + #region Quantum + + if (extension.Equals("q", StringComparison.OrdinalIgnoreCase)) + return SupportedFileType.Quantum; + + // Both PAK and Quantum share one extension + // if (extension.Equals("pak", StringComparison.OrdinalIgnoreCase)) + // return SupportedFileType.Quantum; + + #endregion + #region RAR if (extension.Equals("rar", StringComparison.OrdinalIgnoreCase)) @@ -646,6 +666,7 @@ namespace BurnOutSharp.Tools case SupportedFileType.PAK: return new FileType.PAK(); case SupportedFileType.PKZIP: return new FileType.PKZIP(); case SupportedFileType.PLJ: return new FileType.PLJ(); + case SupportedFileType.Quantum: return null; // TODO: Update this line case SupportedFileType.RAR: return new FileType.RAR(); case SupportedFileType.SevenZip: return new FileType.SevenZip(); case SupportedFileType.SFFS: return new FileType.SFFS(); diff --git a/Test/Program.cs b/Test/Program.cs index b2a14088..47e3edc8 100644 --- a/Test/Program.cs +++ b/Test/Program.cs @@ -439,6 +439,25 @@ namespace Test pak.Print(); } + // Quantum + else if (ft == SupportedFileType.Quantum) + { + // Build the archive information + Console.WriteLine("Creating Quantum deserializer"); + Console.WriteLine(); + + var quantum = Quantum.Create(stream); + if (quantum == null) + { + Console.WriteLine("Something went wrong parsing Quantum"); + Console.WriteLine(); + return; + } + + // Print the Quantum info to screen + quantum.Print(); + } + // SGA else if (ft == SupportedFileType.SGA) {