Add Quantum archive models/builder/wrapper

This commit is contained in:
Matt Nadareski
2023-01-03 09:28:16 -08:00
parent c2c125fd29
commit 2416a035c7
9 changed files with 488 additions and 20 deletions

View File

@@ -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
/// <summary>
/// Parse a byte array into a Quantum archive
/// </summary>
/// <param name="data">Byte array to parse</param>
/// <param name="offset">Offset into the byte array</param>
/// <returns>Filled archive on success, null on error</returns>
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
/// <summary>
/// Parse a Stream into a Quantum archive
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled archive on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a header
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled header on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a file descriptor
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled file descriptor on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a variable-length size prefix
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Variable-length size prefix</returns>
/// <remarks>
/// 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.
/// </remarks>
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
}
}

View File

@@ -0,0 +1,21 @@
namespace BurnOutSharp.Models.Quantum
{
/// <summary>
/// Quantum archive file structure
/// </summary>
/// <see href="https://handwiki.org/wiki/Software:Quantum_compression"/>
public class Archive
{
/// <summary>
/// Quantum header
/// </summary>
public Header Header { get; set; }
/// <summary>
/// This is immediately followed by the list of files
/// </summary>
public FileDescriptor[] FileList { get; set; }
// Immediately following the list of files is the compressed data.
}
}

View File

@@ -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;
}
}

View File

@@ -1,18 +1,26 @@
namespace BurnOutSharp.Models.Compression.Quantum
namespace BurnOutSharp.Models.Quantum
{
/// <remarks>
/// 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.
/// </remarks>
/// <summary>
/// Quantum archive file descriptor
/// </summary>
/// <see href="https://handwiki.org/wiki/Software:Quantum_compression"/>
public class FileDescriptor
{
/// <summary>
/// Length of file name
/// </summary>
public int FileNameSize;
/// <summary>
/// File name, variable length string, not zero-terminated
/// </summary>
public string FileName;
/// <summary>
/// Length of comment field
/// </summary>
public int CommentFieldSize;
/// <summary>
/// Comment field, variable length string, not zero-terminated
/// </summary>

View File

@@ -1,18 +1,18 @@
using System.Runtime.InteropServices;
namespace BurnOutSharp.Models.Compression.Quantum
namespace BurnOutSharp.Models.Quantum
{
/// <summary>
/// Quantum archive file structure
/// Quantum archive file header
/// </summary>
/// <see href="https://handwiki.org/wiki/Software:Quantum_compression"/>
[StructLayout(LayoutKind.Sequential)]
public class Archive
public class Header
{
/// <summary>
/// Quantum signature: 0x44 0x53
/// </summary>
public ushort Signature;
public string Signature;
/// <summary>
/// Quantum major version number
@@ -38,12 +38,5 @@ namespace BurnOutSharp.Models.Compression.Quantum
/// Compression flags
/// </summary>
public byte CompressionFlags;
/// <summary>
/// This is immediately followed by the list of files
/// </summary>
public FileDescriptor[] FileList;
// Immediately following the list of files is the compressed data.
}
}

View File

@@ -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
/// <inheritdoc cref="Models.Quantum.Header.Signature"/>
public string Signature => _archive.Header.Signature;
/// <inheritdoc cref="Models.Quantum.Header.MajorVersion"/>
public byte MajorVersion => _archive.Header.MajorVersion;
/// <inheritdoc cref="Models.Quantum.Header.MinorVersion"/>
public byte MinorVersion => _archive.Header.MinorVersion;
/// <inheritdoc cref="Models.Quantum.Header.FileCount"/>
public ushort FileCount => _archive.Header.FileCount;
/// <inheritdoc cref="Models.Quantum.Header.TableSize"/>
public byte TableSize => _archive.Header.TableSize;
/// <inheritdoc cref="Models.Quantum.Header.CompressionFlags"/>
public byte CompressionFlags => _archive.Header.CompressionFlags;
#endregion
#region Files
/// <inheritdoc cref="Models.Quantum.Archive.FileList"/>
public Models.Quantum.FileDescriptor[] FileList => _archive.FileList;
#endregion
#endregion
#region Instance Variables
/// <summary>
/// Internal representation of the archive
/// </summary>
private Models.Quantum.Archive _archive;
#endregion
#region Constructors
/// <summary>
/// Private constructor
/// </summary>
private Quantum() { }
/// <summary>
/// Create a Quantum archive from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the archive</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>A Quantum archive wrapper on success, null on failure</returns>
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);
}
/// <summary>
/// Create a Quantum archive from a Stream
/// </summary>
/// <param name="data">Stream representing the archive</param>
/// <returns>A Quantum archive wrapper on success, null on failure</returns>
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
/// <summary>
/// Extract all files from the Quantum archive to an output directory
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <returns>True if all files extracted, false otherwise</returns>
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;
}
/// <summary>
/// Extract a file from the Quantum archive to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <returns>True if the file extracted, false otherwise</returns>
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
/// <inheritdoc/>
public override void Print()
{
Console.WriteLine("Quantum Information:");
Console.WriteLine("-------------------------");
Console.WriteLine();
PrintHeader();
PrintFileList();
}
/// <summary>
/// Print header information
/// </summary>
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();
}
/// <summary>
/// Print file list information
/// </summary>
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
}
}

View File

@@ -95,6 +95,11 @@
/// </summary>
PLJ,
/// <summary>
/// Quantum archive
/// </summary>
Quantum,
/// <summary>
/// RAR archive
/// </summary>

View File

@@ -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();

View File

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