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)
{