Add Compression package

This commit is contained in:
Matt Nadareski
2024-12-11 13:31:54 -05:00
parent 01a195c8aa
commit 3de92de225
22 changed files with 1471 additions and 16 deletions

View File

@@ -0,0 +1,73 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Deserializers;
using Xunit;
namespace SabreTools.Serialization.Test.Deserializers
{
public class LZKWAJTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var deserializer = new LZKWAJ();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var deserializer = new LZKWAJ();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var deserializer = new LZKWAJ();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var deserializer = new LZKWAJ();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var deserializer = new LZKWAJ();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var deserializer = new LZKWAJ();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
}
}

View File

@@ -0,0 +1,74 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Deserializers;
using Xunit;
namespace SabreTools.Serialization.Test.Deserializers
{
public class LZQBasicTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var deserializer = new LZQBasic();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var deserializer = new LZQBasic();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var deserializer = new LZQBasic();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var deserializer = new LZQBasic();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var deserializer = new LZQBasic();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var deserializer = new LZQBasic();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
}
}

View File

@@ -0,0 +1,73 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Deserializers;
using Xunit;
namespace SabreTools.Serialization.Test.Deserializers
{
public class LZSZDDTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var deserializer = new LZSZDD();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var deserializer = new LZSZDD();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var deserializer = new LZSZDD();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var deserializer = new LZSZDD();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var deserializer = new LZSZDD();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var deserializer = new LZSZDD();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
}
}

View File

@@ -28,7 +28,7 @@
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="SabreTools.Hashing" Version="1.4.0" />
<PackageReference Include="SabreTools.Models" Version="1.5.6" />
<PackageReference Include="SabreTools.Models" Version="1.5.7" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -0,0 +1,61 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Wrappers;
using Xunit;
namespace SabreTools.Serialization.Test.Wrappers
{
public class LZKWAJTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var actual = LZKWAJ.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var actual = LZKWAJ.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var actual = LZKWAJ.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var actual = LZKWAJ.Create(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var actual = LZKWAJ.Create(data);
Assert.Null(actual);
}
[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var actual = LZKWAJ.Create(data);
Assert.Null(actual);
}
}
}

View File

@@ -0,0 +1,61 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Wrappers;
using Xunit;
namespace SabreTools.Serialization.Test.Wrappers
{
public class LZQBasicTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var actual = LZQBasic.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var actual = LZQBasic.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var actual = LZQBasic.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var actual = LZQBasic.Create(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var actual = LZQBasic.Create(data);
Assert.Null(actual);
}
[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var actual = LZQBasic.Create(data);
Assert.Null(actual);
}
}
}

View File

@@ -0,0 +1,61 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Wrappers;
using Xunit;
namespace SabreTools.Serialization.Test.Wrappers
{
public class LZSZDDTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var actual = LZSZDD.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var actual = LZSZDD.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var actual = LZSZDD.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var actual = LZSZDD.Create(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var actual = LZSZDD.Create(data);
Assert.Null(actual);
}
[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var actual = LZSZDD.Create(data);
Assert.Null(actual);
}
}
}

View File

@@ -0,0 +1,121 @@
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using static SabreTools.Models.LZ.Constants;
namespace SabreTools.Serialization.Deserializers
{
public class LZKWAJ : BaseBinaryDeserializer<KWAJFile>
{
/// <inheritdoc/>
public override KWAJFile? Deserialize(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;
try
{
// Cache the current offset
int initialOffset = (int)data.Position;
// Create a new file to fill
var file = new KWAJFile();
#region File Header
// Try to parse the header
var header = ParseHeader(data);
if (header == null)
return null;
// Set the header
file.Header = header;
#endregion
#region Extended Header
if (header.HeaderFlags != 0)
{
var extensions = new KWAJHeaderExtensions();
#if NET20 || NET35
if ((header.HeaderFlags & KWAJHeaderFlags.HasDecompressedLength) != 0)
extensions.DecompressedLength = data.ReadUInt32();
if ((header.HeaderFlags & KWAJHeaderFlags.HasUnknownFlag) != 0)
extensions.UnknownPurpose = data.ReadUInt16();
if ((header.HeaderFlags & KWAJHeaderFlags.HasPrefixedData) != 0)
{
extensions.UnknownDataLength = data.ReadUInt16();
extensions.UnknownData = data.ReadBytes((int)extensions.UnknownDataLength);
}
if ((header.HeaderFlags & KWAJHeaderFlags.HasFileName) != 0)
extensions.FileName = data.ReadNullTerminatedAnsiString();
if ((header.HeaderFlags & KWAJHeaderFlags.HasFileExtension) != 0)
extensions.FileExtension = data.ReadNullTerminatedAnsiString();
if ((header.HeaderFlags & KWAJHeaderFlags.HasPrefixedData) != 0)
{
extensions.ArbitraryTextLength = data.ReadUInt16();
extensions.ArbitraryText = data.ReadBytes((int)extensions.ArbitraryTextLength);
}
#else
if (header.HeaderFlags.HasFlag(KWAJHeaderFlags.HasDecompressedLength))
extensions.DecompressedLength = data.ReadUInt32();
if (header.HeaderFlags.HasFlag(KWAJHeaderFlags.HasUnknownFlag))
extensions.UnknownPurpose = data.ReadUInt16();
if (header.HeaderFlags.HasFlag(KWAJHeaderFlags.HasPrefixedData))
{
extensions.UnknownDataLength = data.ReadUInt16();
extensions.UnknownData = data.ReadBytes((int)extensions.UnknownDataLength);
}
if (header.HeaderFlags.HasFlag(KWAJHeaderFlags.HasFileName))
extensions.FileName = data.ReadNullTerminatedAnsiString();
if (header.HeaderFlags.HasFlag(KWAJHeaderFlags.HasFileExtension))
extensions.FileExtension = data.ReadNullTerminatedAnsiString();
if (header.HeaderFlags.HasFlag(KWAJHeaderFlags.HasPrefixedData))
{
extensions.ArbitraryTextLength = data.ReadUInt16();
extensions.ArbitraryText = data.ReadBytes((int)extensions.ArbitraryTextLength);
}
#endif
file.HeaderExtensions = extensions;
}
#endregion
return file;
}
catch
{
// Ignore the actual error
return null;
}
}
/// <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 KWAJHeader? ParseHeader(Stream data)
{
var header = new KWAJHeader();
header.Magic = data.ReadBytes(8);
if (Encoding.ASCII.GetString(header.Magic) != Encoding.ASCII.GetString(KWAJSignatureBytes))
return null;
header.CompressionType = (KWAJCompressionType)data.ReadUInt16();
if (header.CompressionType > KWAJCompressionType.MSZIP)
return null;
header.DataOffset = data.ReadUInt16();
header.HeaderFlags = (KWAJHeaderFlags)data.ReadUInt16();
return header;
}
}
}

View File

@@ -0,0 +1,65 @@
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using static SabreTools.Models.LZ.Constants;
namespace SabreTools.Serialization.Deserializers
{
public class LZQBasic : BaseBinaryDeserializer<QBasicFile>
{
/// <inheritdoc/>
public override QBasicFile? Deserialize(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;
try
{
// Cache the current offset
int initialOffset = (int)data.Position;
// Create a new file to fill
var file = new QBasicFile();
#region File Header
// Try to parse the header
var header = ParseHeader(data);
if (header == null)
return null;
// Set the header
file.Header = header;
#endregion
return file;
}
catch
{
// Ignore the actual error
return null;
}
}
/// <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 QBasicHeader? ParseHeader(Stream data)
{
var header = new QBasicHeader();
header.Magic = data.ReadBytes(8);
if (Encoding.ASCII.GetString(header.Magic) != Encoding.ASCII.GetString(QBasicSignatureBytes))
return null;
header.RealLength = data.ReadUInt32();
return header;
}
}
}

View File

@@ -0,0 +1,70 @@
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using static SabreTools.Models.LZ.Constants;
namespace SabreTools.Serialization.Deserializers
{
public class LZSZDD : BaseBinaryDeserializer<SZDDFile>
{
/// <inheritdoc/>
public override SZDDFile? Deserialize(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;
try
{
// Cache the current offset
int initialOffset = (int)data.Position;
// Create a new file to fill
var file = new SZDDFile();
#region File Header
// Try to parse the header
var header = ParseHeader(data);
if (header == null)
return null;
// Set the header
file.Header = header;
#endregion
return file;
}
catch
{
// Ignore the actual error
return null;
}
}
/// <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 SZDDHeader? ParseHeader(Stream data)
{
var header = new SZDDHeader();
header.Magic = data.ReadBytes(8);
if (Encoding.ASCII.GetString(header.Magic) != Encoding.ASCII.GetString(SZDDSignatureBytes))
return null;
header.CompressionType = (ExpandCompressionType)data.ReadByteValue();
if (header.CompressionType != ExpandCompressionType.A)
return null;
header.LastChar = (char)data.ReadByteValue();
header.RealLength = data.ReadUInt32();
return header;
}
}
}

View File

@@ -45,6 +45,9 @@ namespace SabreTools.Serialization
Wrapper.InstallShieldCabinet item => item.PrettyPrint(),
Wrapper.IRD item => item.PrettyPrint(),
Wrapper.LinearExecutable item => item.PrettyPrint(),
Wrapper.LZKWAJ item => item.PrettyPrint(),
Wrapper.LZQBasic item => item.PrettyPrint(),
Wrapper.LZSZDD item => item.PrettyPrint(),
Wrapper.MicrosoftCabinet item => item.PrettyPrint(),
Wrapper.MoPaQ item => item.PrettyPrint(),
Wrapper.MSDOS item => item.PrettyPrint(),
@@ -90,6 +93,9 @@ namespace SabreTools.Serialization
Wrapper.InstallShieldCabinet item => item.ExportJSON(),
Wrapper.IRD item => item.ExportJSON(),
Wrapper.LinearExecutable item => item.ExportJSON(),
Wrapper.LZKWAJ item => item.ExportJSON(),
Wrapper.LZQBasic item => item.ExportJSON(),
Wrapper.LZSZDD item => item.ExportJSON(),
Wrapper.MicrosoftCabinet item => item.ExportJSON(),
Wrapper.MoPaQ item => item.ExportJSON(),
Wrapper.MSDOS item => item.ExportJSON(),
@@ -229,6 +235,36 @@ namespace SabreTools.Serialization
return builder;
}
/// <summary>
/// Export the item information as pretty-printed text
/// </summary>
private static StringBuilder PrettyPrint(this Wrapper.LZKWAJ item)
{
var builder = new StringBuilder();
LZKWAJ.Print(builder, item.Model);
return builder;
}
/// <summary>
/// Export the item information as pretty-printed text
/// </summary>
private static StringBuilder PrettyPrint(this Wrapper.LZQBasic item)
{
var builder = new StringBuilder();
LZQBasic.Print(builder, item.Model);
return builder;
}
/// <summary>
/// Export the item information as pretty-printed text
/// </summary>
private static StringBuilder PrettyPrint(this Wrapper.LZSZDD item)
{
var builder = new StringBuilder();
LZSZDD.Print(builder, item.Model);
return builder;
}
/// <summary>
/// Export the item information as pretty-printed text
/// </summary>

View File

@@ -0,0 +1,63 @@
using System.Text;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Printers
{
public class LZKWAJ : IPrinter<KWAJFile>
{
/// <inheritdoc/>
public void PrintInformation(StringBuilder builder, KWAJFile model)
=> Print(builder, model);
public static void Print(StringBuilder builder, KWAJFile file)
{
builder.AppendLine("LZ-compressed File, KWAJ Variant Information:");
builder.AppendLine("-------------------------");
builder.AppendLine();
Print(builder, file.Header);
Print(builder, file.HeaderExtensions);
}
private static void Print(StringBuilder builder, KWAJHeader? header)
{
builder.AppendLine(" Header Information:");
builder.AppendLine(" -------------------------");
if (header == null)
{
builder.AppendLine(" No header");
builder.AppendLine();
return;
}
builder.AppendLine(header.Magic, " Magic number");
builder.AppendLine($" Compression type: {header.CompressionType} (0x{header.CompressionType:X})");
builder.AppendLine(header.DataOffset, " Data offset");
builder.AppendLine($" Header flags: {header.HeaderFlags} (0x{header.HeaderFlags:X})");
builder.AppendLine();
}
private static void Print(StringBuilder builder, KWAJHeaderExtensions? header)
{
builder.AppendLine(" Header Extensions Information:");
builder.AppendLine(" -------------------------");
if (header == null)
{
builder.AppendLine(" No header extensions");
builder.AppendLine();
return;
}
builder.AppendLine(header.DecompressedLength, " Decompressed length");
builder.AppendLine(header.UnknownPurpose, " Unknown purpose");
builder.AppendLine(header.UnknownDataLength, " Unknown data length");
builder.AppendLine(header.UnknownData, " Unknown data");
builder.AppendLine(header.FileName, " File name");
builder.AppendLine(header.FileExtension, " File extension");
builder.AppendLine(header.ArbitraryTextLength, " Arbitrary text length");
builder.AppendLine(header.ArbitraryText, " Arbitrary text");
builder.AppendLine();
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Text;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Printers
{
public class LZQBasic : IPrinter<QBasicFile>
{
/// <inheritdoc/>
public void PrintInformation(StringBuilder builder, QBasicFile model)
=> Print(builder, model);
public static void Print(StringBuilder builder, QBasicFile file)
{
builder.AppendLine("LZ-compressed File, QBasic Variant Information:");
builder.AppendLine("-------------------------");
builder.AppendLine();
Print(builder, file.Header);
}
private static void Print(StringBuilder builder, QBasicHeader? header)
{
builder.AppendLine(" Header Information:");
builder.AppendLine(" -------------------------");
if (header == null)
{
builder.AppendLine(" No header");
builder.AppendLine();
return;
}
builder.AppendLine(header.Magic, " Magic number");
builder.AppendLine(header.RealLength, " Real length");
builder.AppendLine();
}
}
}

View File

@@ -0,0 +1,40 @@
using System.Text;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Printers
{
public class LZSZDD : IPrinter<SZDDFile>
{
/// <inheritdoc/>
public void PrintInformation(StringBuilder builder, SZDDFile model)
=> Print(builder, model);
public static void Print(StringBuilder builder, SZDDFile file)
{
builder.AppendLine("LZ-compressed File, SZDD Variant Information:");
builder.AppendLine("-------------------------");
builder.AppendLine();
Print(builder, file.Header);
}
private static void Print(StringBuilder builder, SZDDHeader? header)
{
builder.AppendLine(" Header Information:");
builder.AppendLine(" -------------------------");
if (header == null)
{
builder.AppendLine(" No header");
builder.AppendLine();
return;
}
builder.AppendLine(header.Magic, " Magic number");
builder.AppendLine($" Compression type: {header.CompressionType} (0x{header.CompressionType:X})");
builder.AppendLine(header.LastChar, " Last char");
builder.AppendLine(header.RealLength, " Real length");
builder.AppendLine();
}
}
}

View File

@@ -31,9 +31,10 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SabreTools.ASN1" Version="1.5.0" />
<PackageReference Include="SabreTools.Compression" Version="0.6.2" />
<PackageReference Include="SabreTools.Hashing" Version="1.4.0" />
<PackageReference Include="SabreTools.IO" Version="1.6.1" />
<PackageReference Include="SabreTools.Models" Version="1.5.6" />
<PackageReference Include="SabreTools.Models" Version="1.5.7" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,139 @@
using System.IO;
using SabreTools.Compression.SZDD;
using SabreTools.Models.LZ;
namespace SabreTools.Serialization.Wrappers
{
public class LZKWAJ : WrapperBase<KWAJFile>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "LZ-compressed file, KWAJ variant";
#endregion
#region Constructors
/// <inheritdoc/>
public LZKWAJ(KWAJFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
/// <inheritdoc/>
public LZKWAJ(KWAJFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
/// <summary>
/// Create an LZ (KWAJ variant) from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the LZ (KWAJ variant)</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>An LZ (KWAJ variant) wrapper on success, null on failure</returns>
public static LZKWAJ? Create(byte[]? data, int offset)
{
// If the data is invalid
if (data == null || data.Length == 0)
return null;
// If the offset is out of bounds
if (offset < 0 || offset >= data.Length)
return null;
// Create a memory stream and use that
var dataStream = new MemoryStream(data, offset, data.Length - offset);
return Create(dataStream);
}
/// <summary>
/// Create a LZ (KWAJ variant) from a Stream
/// </summary>
/// <param name="data">Stream representing the LZ (KWAJ variant)</param>
/// <returns>An LZ (KWAJ variant) wrapper on success, null on failure</returns>
public static LZKWAJ? Create(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;
try
{
var file = Deserializers.LZKWAJ.DeserializeStream(data);
if (file == null)
return null;
return new LZKWAJ(file, data);
}
catch
{
return null;
}
}
#endregion
#region Extraction
/// <summary>
/// Extract the contents to an output directory
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <returns>True if the contents extracted, false otherwise</returns>
public bool Extract(string outputDirectory)
{
// Get the length of the compressed data
long compressedSize = Length - Model.Header!.DataOffset;
if (compressedSize < Model.Header.DataOffset)
return false;
// Read in the data as an array
byte[]? contents = ReadFromDataSource(Model.Header.DataOffset, (int)compressedSize);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateKWAJ(contents, Model.Header!.CompressionType);
if (decompressor == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the full output path
string filename = "tempfile";
if (Model.HeaderExtensions?.FileName != null)
filename = Model.HeaderExtensions.FileName;
if (Model.HeaderExtensions?.FileExtension != null)
filename += $".{Model.HeaderExtensions.FileExtension}";
filename = Path.Combine(outputDirectory, filename);
// Ensure the output directory is created
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null)
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
}
catch
{
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,134 @@
using System.IO;
using SabreTools.Compression.SZDD;
using SabreTools.Models.LZ;
namespace SabreTools.Serialization.Wrappers
{
public class LZQBasic : WrapperBase<QBasicFile>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "LZ-compressed file, QBasic variant";
#endregion
#region Constructors
/// <inheritdoc/>
public LZQBasic(QBasicFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
/// <inheritdoc/>
public LZQBasic(QBasicFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
/// <summary>
/// Create an LZ (QBasic variant) from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the LZ (QBasic variant)</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>An LZ (QBasic variant) wrapper on success, null on failure</returns>
public static LZQBasic? Create(byte[]? data, int offset)
{
// If the data is invalid
if (data == null || data.Length == 0)
return null;
// If the offset is out of bounds
if (offset < 0 || offset >= data.Length)
return null;
// Create a memory stream and use that
var dataStream = new MemoryStream(data, offset, data.Length - offset);
return Create(dataStream);
}
/// <summary>
/// Create a LZ (QBasic variant) from a Stream
/// </summary>
/// <param name="data">Stream representing the LZ (QBasic variant)</param>
/// <returns>An LZ (QBasic variant) wrapper on success, null on failure</returns>
public static LZQBasic? Create(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;
try
{
var file = Deserializers.LZQBasic.DeserializeStream(data);
if (file == null)
return null;
return new LZQBasic(file, data);
}
catch
{
return null;
}
}
#endregion
#region Extraction
/// <summary>
/// Extract the contents to an output directory
/// </summary>
/// <param name="filename">Original filename to use as a base</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <returns>True if the contents extracted, false otherwise</returns>
public bool Extract(string outputDirectory)
{
// Get the length of the compressed data
long compressedSize = Length - 12;
if (compressedSize < 12)
return false;
// Read in the data as an array
byte[]? contents = ReadFromDataSource(12, (int)compressedSize);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateQBasic(contents);
if (decompressor == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the full output path
string filename = Path.Combine(outputDirectory, "tempfile.bin");
// Ensure the output directory is created
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null)
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
}
catch
{
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,166 @@
using System.IO;
using SabreTools.Compression.SZDD;
using SabreTools.Models.LZ;
namespace SabreTools.Serialization.Wrappers
{
public class LZSZDD : WrapperBase<SZDDFile>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "LZ-compressed file, SZDD variant";
#endregion
#region Constructors
/// <inheritdoc/>
public LZSZDD(SZDDFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
/// <inheritdoc/>
public LZSZDD(SZDDFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
/// <summary>
/// Create an LZ (SZDD variant) from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the LZ (SZDD variant)</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>An LZ (SZDD variant) wrapper on success, null on failure</returns>
public static LZSZDD? Create(byte[]? data, int offset)
{
// If the data is invalid
if (data == null || data.Length == 0)
return null;
// If the offset is out of bounds
if (offset < 0 || offset >= data.Length)
return null;
// Create a memory stream and use that
var dataStream = new MemoryStream(data, offset, data.Length - offset);
return Create(dataStream);
}
/// <summary>
/// Create a LZ (SZDD variant) from a Stream
/// </summary>
/// <param name="data">Stream representing the LZ (SZDD variant)</param>
/// <returns>An LZ (SZDD variant) wrapper on success, null on failure</returns>
public static LZSZDD? Create(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;
try
{
var file = Deserializers.LZSZDD.DeserializeStream(data);
if (file == null)
return null;
return new LZSZDD(file, data);
}
catch
{
return null;
}
}
#endregion
#region Extraction
/// <summary>
/// Extract the contents to an output directory
/// </summary>
/// <param name="filename">Original filename to use as a base</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <returns>True if the contents extracted, false otherwise</returns>
public bool Extract(string filename, string outputDirectory)
{
// Get the length of the compressed data
long compressedSize = Length - 14;
if (compressedSize < 14)
return false;
// Read in the data as an array
byte[]? contents = ReadFromDataSource(14, (int)compressedSize);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateSZDD(contents);
if (decompressor == null)
return false;
// Create the output file
filename = GetExpandedName(filename).TrimEnd('\0');
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the full output path
filename = Path.Combine(outputDirectory, filename);
// Ensure the output directory is created
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null)
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
}
catch
{
return false;
}
return true;
}
/// <summary>
/// Get the full name of the input file
/// </summary>
private string GetExpandedName(string input)
{
// If the extension is missing
string extension = Path.GetExtension(input).TrimStart('.');
if (string.IsNullOrEmpty(extension))
return Path.GetFileNameWithoutExtension(input);
// If the extension is a single character
if (extension.Length == 1)
{
if (extension == "_" || extension == "$")
return $"{Path.GetFileNameWithoutExtension(input)}.{char.ToLower(Model.Header!.LastChar)}";
return Path.GetFileNameWithoutExtension(input);
}
// If the extension isn't formatted
if (!extension.EndsWith("_"))
return Path.GetFileNameWithoutExtension(input);
// Handle replacing characters
char c = (char.IsUpper(input[0]) ? char.ToLower(Model.Header!.LastChar) : char.ToUpper(Model.Header!.LastChar));
string text2 = extension.Substring(0, extension.Length - 1) + c;
return Path.GetFileNameWithoutExtension(input) + "." + text2;
}
#endregion
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.Compression.zlib;
using SabreTools.Models.SGA;
namespace SabreTools.Serialization.Wrappers
@@ -120,6 +122,148 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <summary>
/// Extract all files from the SGA 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)
{
// Get the file count
int fileCount = FileCount;
if (fileCount == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < fileCount; i++)
{
allExtracted &= ExtractFile(i, outputDirectory);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the SGA 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)
{
// Get the file count
int fileCount = FileCount;
if (fileCount == 0)
return false;
// If the files index is invalid
if (index < 0 || index >= fileCount)
return false;
// Create the filename
var filename = GetFileName(index);
if (filename == null)
return false;
// Loop through and get all parent directories
var parentNames = new List<string> { filename };
// Get the parent directory
string? folderName = GetParentName(index);
if (folderName != null)
parentNames.Add(folderName);
// TODO: Should the section name/alias be used in the path as well?
// Reverse and assemble the filename
parentNames.Reverse();
#if NET20 || NET35
filename = parentNames[0];
for (int i = 1; i < parentNames.Count; i++)
{
filename = Path.Combine(filename, parentNames[i]);
}
#else
filename = Path.Combine([.. parentNames]);
#endif
// Get and adjust the file offset
long fileOffset = GetFileOffset(index);
fileOffset += FileDataOffset;
if (fileOffset < 0)
return false;
// Get the file sizes
long fileSize = GetCompressedSize(index);
long outputFileSize = GetUncompressedSize(index);
// Read the compressed data directly
var compressedData = ReadFromDataSource((int)fileOffset, (int)fileSize);
if (compressedData == null)
return false;
// If the compressed and uncompressed sizes match
byte[] data;
if (fileSize == outputFileSize)
{
data = compressedData;
}
else
{
// Inflate the data into the buffer
var zstream = new ZLib.z_stream_s();
data = new byte[outputFileSize];
unsafe
{
fixed (byte* payloadPtr = compressedData)
fixed (byte* dataPtr = data)
{
zstream.next_in = payloadPtr;
zstream.avail_in = (uint)compressedData.Length;
zstream.total_in = (uint)compressedData.Length;
zstream.next_out = dataPtr;
zstream.avail_out = (uint)data.Length;
zstream.total_out = 0;
ZLib.inflateInit_(zstream, ZLib.zlibVersion(), compressedData.Length);
int zret = ZLib.inflate(zstream, 1);
ZLib.inflateEnd(zstream);
}
}
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the full output path
filename = Path.Combine(outputDirectory, filename);
// Ensure the output directory is created
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null)
System.IO.Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
}
catch
{
return false;
}
return false;
}
#endregion
#region File
/// <summary>

View File

@@ -45,6 +45,24 @@ namespace SabreTools.Serialization.Wrappers
/// </summary>
public T Model { get; private set; }
/// <summary>
/// Length of the underlying data
/// </summary>
public long Length
{
get
{
return _dataSource switch
{
DataSource.ByteArray => _byteArrayData!.Length - _byteArrayOffset,
DataSource.Stream => _streamData!.Length,
// Everything else is invalid
_ => -1,
};
}
}
#endregion
#region Instance Variables

View File

@@ -31,8 +31,10 @@ namespace SabreTools.Serialization.Wrappers
WrapperType.InstallShieldArchiveV3 => null,// TODO: Implement wrapper
WrapperType.InstallShieldCAB => InstallShieldCabinet.Create(data),
WrapperType.LDSCRYPT => null,// TODO: Implement wrapper
WrapperType.LZKWAJ => LZKWAJ.Create(data),
WrapperType.LZQBasic => LZQBasic.Create(data),
WrapperType.LZSZDD => LZSZDD.Create(data),
WrapperType.MicrosoftCAB => MicrosoftCabinet.Create(data),
WrapperType.MicrosoftLZ => null,// TODO: Implement wrapper
WrapperType.MoPaQ => MoPaQ.Create(data),
WrapperType.N3DS => N3DS.Create(data),
WrapperType.NCF => NCF.Create(data),
@@ -330,6 +332,19 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region LZ
if (magic.StartsWith([0x4B, 0x57, 0x41, 0x4A, 0x88, 0xF0, 0x27, 0xD1]))
return WrapperType.LZKWAJ;
if (magic.StartsWith([0x53, 0x5A, 0x20, 0x88, 0xF0, 0x27, 0x33, 0xD1]))
return WrapperType.LZQBasic;
if (magic.StartsWith([0x53, 0x5A, 0x44, 0x44, 0x88, 0xF0, 0x27, 0x33]))
return WrapperType.LZSZDD;
#endregion
#region MicrosoftCAB
if (magic.StartsWith([0x4d, 0x53, 0x43, 0x46]))
@@ -339,13 +354,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region MicrosoftLZ
if (magic.StartsWith([0x53, 0x5a, 0x44, 0x44, 0x88, 0xf0, 0x27, 0x33]))
return WrapperType.MicrosoftLZ;
#endregion
#region MoPaQ
if (magic.StartsWith([0x4d, 0x50, 0x51, 0x1a]))

View File

@@ -91,17 +91,26 @@ namespace SabreTools.Serialization.Wrappers
/// <remarks>Currently has no IWrapper implementation</remarks>
LDSCRYPT,
/// <summary>
/// LZ-compressed file, KWAJ variant
/// </summary>
LZKWAJ,
/// <summary>
/// LZ-compressed file, QBasic variant
/// </summary>
LZQBasic,
/// <summary>
/// LZ-compressed file, SZDD variant
/// </summary>
LZSZDD,
/// <summary>
/// Microsoft cabinet file
/// </summary>
MicrosoftCAB,
/// <summary>
/// Microsoft LZ-compressed file
/// </summary>
/// <remarks>Currently has no IWrapper implementation</remarks>
MicrosoftLZ,
/// <summary>
/// MPQ game data archive
/// </summary>