mirror of
https://github.com/SabreTools/BinaryObjectScanner.git
synced 2026-07-09 02:16:46 +00:00
Add Quantum archive models/builder/wrapper
This commit is contained in:
176
BurnOutSharp.Builders/Quantum.cs
Normal file
176
BurnOutSharp.Builders/Quantum.cs
Normal 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
|
||||
}
|
||||
}
|
||||
21
BurnOutSharp.Models/Quantum/Archive.cs
Normal file
21
BurnOutSharp.Models/Quantum/Archive.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
11
BurnOutSharp.Models/Quantum/Constants.cs
Normal file
11
BurnOutSharp.Models/Quantum/Constants.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
214
BurnOutSharp.Wrappers/Quantum.cs
Normal file
214
BurnOutSharp.Wrappers/Quantum.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,11 @@
|
||||
/// </summary>
|
||||
PLJ,
|
||||
|
||||
/// <summary>
|
||||
/// Quantum archive
|
||||
/// </summary>
|
||||
Quantum,
|
||||
|
||||
/// <summary>
|
||||
/// RAR archive
|
||||
/// </summary>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user