diff --git a/SabreTools.Serialization.Test/Deserializers/LZKWAJTests.cs b/SabreTools.Serialization.Test/Deserializers/LZKWAJTests.cs new file mode 100644 index 00000000..468c5d14 --- /dev/null +++ b/SabreTools.Serialization.Test/Deserializers/LZKWAJTests.cs @@ -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(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(0xFF, 1024)]); + var deserializer = new LZKWAJ(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization.Test/Deserializers/LZQBasicTests.cs b/SabreTools.Serialization.Test/Deserializers/LZQBasicTests.cs new file mode 100644 index 00000000..8db78cbe --- /dev/null +++ b/SabreTools.Serialization.Test/Deserializers/LZQBasicTests.cs @@ -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(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(0xFF, 1024)]); + var deserializer = new LZQBasic(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization.Test/Deserializers/LZSZDDTests.cs b/SabreTools.Serialization.Test/Deserializers/LZSZDDTests.cs new file mode 100644 index 00000000..72109fac --- /dev/null +++ b/SabreTools.Serialization.Test/Deserializers/LZSZDDTests.cs @@ -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(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(0xFF, 1024)]); + var deserializer = new LZSZDD(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization.Test/SabreTools.Serialization.Test.csproj b/SabreTools.Serialization.Test/SabreTools.Serialization.Test.csproj index 81b72916..e11f8a26 100644 --- a/SabreTools.Serialization.Test/SabreTools.Serialization.Test.csproj +++ b/SabreTools.Serialization.Test/SabreTools.Serialization.Test.csproj @@ -28,7 +28,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/SabreTools.Serialization.Test/Wrappers/LZKWAJTests.cs b/SabreTools.Serialization.Test/Wrappers/LZKWAJTests.cs new file mode 100644 index 00000000..750f15b5 --- /dev/null +++ b/SabreTools.Serialization.Test/Wrappers/LZKWAJTests.cs @@ -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(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(0xFF, 1024)]); + var actual = LZKWAJ.Create(data); + Assert.Null(actual); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization.Test/Wrappers/LZQBasicTests.cs b/SabreTools.Serialization.Test/Wrappers/LZQBasicTests.cs new file mode 100644 index 00000000..98afa9f9 --- /dev/null +++ b/SabreTools.Serialization.Test/Wrappers/LZQBasicTests.cs @@ -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(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(0xFF, 1024)]); + var actual = LZQBasic.Create(data); + Assert.Null(actual); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization.Test/Wrappers/LZSZDDTests.cs b/SabreTools.Serialization.Test/Wrappers/LZSZDDTests.cs new file mode 100644 index 00000000..ec20728e --- /dev/null +++ b/SabreTools.Serialization.Test/Wrappers/LZSZDDTests.cs @@ -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(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(0xFF, 1024)]); + var actual = LZSZDD.Create(data); + Assert.Null(actual); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Deserializers/LZKWAJ.cs b/SabreTools.Serialization/Deserializers/LZKWAJ.cs new file mode 100644 index 00000000..0055b4a9 --- /dev/null +++ b/SabreTools.Serialization/Deserializers/LZKWAJ.cs @@ -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 + { + /// + 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; + } + } + + /// + /// Parse a Stream into a header + /// + /// Stream to parse + /// Filled header on success, null on error + 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; + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Deserializers/LZQBasic.cs b/SabreTools.Serialization/Deserializers/LZQBasic.cs new file mode 100644 index 00000000..cc542ccd --- /dev/null +++ b/SabreTools.Serialization/Deserializers/LZQBasic.cs @@ -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 + { + /// + 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; + } + } + + /// + /// Parse a Stream into a header + /// + /// Stream to parse + /// Filled header on success, null on error + 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; + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Deserializers/LZSZDD.cs b/SabreTools.Serialization/Deserializers/LZSZDD.cs new file mode 100644 index 00000000..84b15dee --- /dev/null +++ b/SabreTools.Serialization/Deserializers/LZSZDD.cs @@ -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 + { + /// + 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; + } + } + + /// + /// Parse a Stream into a header + /// + /// Stream to parse + /// Filled header on success, null on error + 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; + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Printer.cs b/SabreTools.Serialization/Printer.cs index f13374c4..9f608545 100644 --- a/SabreTools.Serialization/Printer.cs +++ b/SabreTools.Serialization/Printer.cs @@ -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; } + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this Wrapper.LZKWAJ item) + { + var builder = new StringBuilder(); + LZKWAJ.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this Wrapper.LZQBasic item) + { + var builder = new StringBuilder(); + LZQBasic.Print(builder, item.Model); + return builder; + } + + /// + /// Export the item information as pretty-printed text + /// + private static StringBuilder PrettyPrint(this Wrapper.LZSZDD item) + { + var builder = new StringBuilder(); + LZSZDD.Print(builder, item.Model); + return builder; + } + /// /// Export the item information as pretty-printed text /// diff --git a/SabreTools.Serialization/Printers/LZKWAJ.cs b/SabreTools.Serialization/Printers/LZKWAJ.cs new file mode 100644 index 00000000..7802a89b --- /dev/null +++ b/SabreTools.Serialization/Printers/LZKWAJ.cs @@ -0,0 +1,63 @@ +using System.Text; +using SabreTools.Models.LZ; +using SabreTools.Serialization.Interfaces; + +namespace SabreTools.Serialization.Printers +{ + public class LZKWAJ : IPrinter + { + /// + 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(); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Printers/LZQBasic.cs b/SabreTools.Serialization/Printers/LZQBasic.cs new file mode 100644 index 00000000..80357e98 --- /dev/null +++ b/SabreTools.Serialization/Printers/LZQBasic.cs @@ -0,0 +1,38 @@ +using System.Text; +using SabreTools.Models.LZ; +using SabreTools.Serialization.Interfaces; + +namespace SabreTools.Serialization.Printers +{ + public class LZQBasic : IPrinter + { + /// + 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(); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Printers/LZSZDD.cs b/SabreTools.Serialization/Printers/LZSZDD.cs new file mode 100644 index 00000000..23278e43 --- /dev/null +++ b/SabreTools.Serialization/Printers/LZSZDD.cs @@ -0,0 +1,40 @@ +using System.Text; +using SabreTools.Models.LZ; +using SabreTools.Serialization.Interfaces; + +namespace SabreTools.Serialization.Printers +{ + public class LZSZDD : IPrinter + { + /// + 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(); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/SabreTools.Serialization.csproj b/SabreTools.Serialization/SabreTools.Serialization.csproj index 8ea433cb..cc42f87f 100644 --- a/SabreTools.Serialization/SabreTools.Serialization.csproj +++ b/SabreTools.Serialization/SabreTools.Serialization.csproj @@ -31,9 +31,10 @@ + - + \ No newline at end of file diff --git a/SabreTools.Serialization/Wrappers/LZKWAJ.cs b/SabreTools.Serialization/Wrappers/LZKWAJ.cs new file mode 100644 index 00000000..aa585757 --- /dev/null +++ b/SabreTools.Serialization/Wrappers/LZKWAJ.cs @@ -0,0 +1,139 @@ +using System.IO; +using SabreTools.Compression.SZDD; +using SabreTools.Models.LZ; + +namespace SabreTools.Serialization.Wrappers +{ + public class LZKWAJ : WrapperBase + { + #region Descriptive Properties + + /// + public override string DescriptionString => "LZ-compressed file, KWAJ variant"; + + #endregion + + #region Constructors + + /// + public LZKWAJ(KWAJFile? model, byte[]? data, int offset) + : base(model, data, offset) + { + // All logic is handled by the base class + } + + /// + public LZKWAJ(KWAJFile? model, Stream? data) + : base(model, data) + { + // All logic is handled by the base class + } + + /// + /// Create an LZ (KWAJ variant) from a byte array and offset + /// + /// Byte array representing the LZ (KWAJ variant) + /// Offset within the array to parse + /// An LZ (KWAJ variant) wrapper on success, null on failure + 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); + } + + /// + /// Create a LZ (KWAJ variant) from a Stream + /// + /// Stream representing the LZ (KWAJ variant) + /// An LZ (KWAJ variant) wrapper on success, null on failure + 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 + + /// + /// Extract the contents to an output directory + /// + /// Output directory to write to + /// True if the contents extracted, false otherwise + 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 + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Wrappers/LZQBasic.cs b/SabreTools.Serialization/Wrappers/LZQBasic.cs new file mode 100644 index 00000000..35dbcf85 --- /dev/null +++ b/SabreTools.Serialization/Wrappers/LZQBasic.cs @@ -0,0 +1,134 @@ +using System.IO; +using SabreTools.Compression.SZDD; +using SabreTools.Models.LZ; + +namespace SabreTools.Serialization.Wrappers +{ + public class LZQBasic : WrapperBase + { + #region Descriptive Properties + + /// + public override string DescriptionString => "LZ-compressed file, QBasic variant"; + + #endregion + + #region Constructors + + /// + public LZQBasic(QBasicFile? model, byte[]? data, int offset) + : base(model, data, offset) + { + // All logic is handled by the base class + } + + /// + public LZQBasic(QBasicFile? model, Stream? data) + : base(model, data) + { + // All logic is handled by the base class + } + + /// + /// Create an LZ (QBasic variant) from a byte array and offset + /// + /// Byte array representing the LZ (QBasic variant) + /// Offset within the array to parse + /// An LZ (QBasic variant) wrapper on success, null on failure + 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); + } + + /// + /// Create a LZ (QBasic variant) from a Stream + /// + /// Stream representing the LZ (QBasic variant) + /// An LZ (QBasic variant) wrapper on success, null on failure + 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 + + /// + /// Extract the contents to an output directory + /// + /// Original filename to use as a base + /// Output directory to write to + /// True if the contents extracted, false otherwise + 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 + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Wrappers/LZSZDD.cs b/SabreTools.Serialization/Wrappers/LZSZDD.cs new file mode 100644 index 00000000..6ea1275f --- /dev/null +++ b/SabreTools.Serialization/Wrappers/LZSZDD.cs @@ -0,0 +1,166 @@ +using System.IO; +using SabreTools.Compression.SZDD; +using SabreTools.Models.LZ; + +namespace SabreTools.Serialization.Wrappers +{ + public class LZSZDD : WrapperBase + { + #region Descriptive Properties + + /// + public override string DescriptionString => "LZ-compressed file, SZDD variant"; + + #endregion + + #region Constructors + + /// + public LZSZDD(SZDDFile? model, byte[]? data, int offset) + : base(model, data, offset) + { + // All logic is handled by the base class + } + + /// + public LZSZDD(SZDDFile? model, Stream? data) + : base(model, data) + { + // All logic is handled by the base class + } + + /// + /// Create an LZ (SZDD variant) from a byte array and offset + /// + /// Byte array representing the LZ (SZDD variant) + /// Offset within the array to parse + /// An LZ (SZDD variant) wrapper on success, null on failure + 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); + } + + /// + /// Create a LZ (SZDD variant) from a Stream + /// + /// Stream representing the LZ (SZDD variant) + /// An LZ (SZDD variant) wrapper on success, null on failure + 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 + + /// + /// Extract the contents to an output directory + /// + /// Original filename to use as a base + /// Output directory to write to + /// True if the contents extracted, false otherwise + 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; + } + + /// + /// Get the full name of the input file + /// + 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 + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Wrappers/SGA.cs b/SabreTools.Serialization/Wrappers/SGA.cs index 2685ad11..4be48d42 100644 --- a/SabreTools.Serialization/Wrappers/SGA.cs +++ b/SabreTools.Serialization/Wrappers/SGA.cs @@ -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 + + /// + /// Extract all files from the SGA to an output directory + /// + /// Output directory to write to + /// True if all files extracted, false otherwise + 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; + } + + /// + /// Extract a file from the SGA 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) + { + // 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 { 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 /// diff --git a/SabreTools.Serialization/Wrappers/WrapperBase.cs b/SabreTools.Serialization/Wrappers/WrapperBase.cs index baeb4efd..4e72e62e 100644 --- a/SabreTools.Serialization/Wrappers/WrapperBase.cs +++ b/SabreTools.Serialization/Wrappers/WrapperBase.cs @@ -45,6 +45,24 @@ namespace SabreTools.Serialization.Wrappers /// public T Model { get; private set; } + /// + /// Length of the underlying data + /// + 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 diff --git a/SabreTools.Serialization/Wrappers/WrapperFactory.cs b/SabreTools.Serialization/Wrappers/WrapperFactory.cs index f5878f11..c2490aa0 100644 --- a/SabreTools.Serialization/Wrappers/WrapperFactory.cs +++ b/SabreTools.Serialization/Wrappers/WrapperFactory.cs @@ -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])) diff --git a/SabreTools.Serialization/Wrappers/WrapperType.cs b/SabreTools.Serialization/Wrappers/WrapperType.cs index 5d9d94fd..3bb31f99 100644 --- a/SabreTools.Serialization/Wrappers/WrapperType.cs +++ b/SabreTools.Serialization/Wrappers/WrapperType.cs @@ -91,17 +91,26 @@ namespace SabreTools.Serialization.Wrappers /// Currently has no IWrapper implementation LDSCRYPT, + /// + /// LZ-compressed file, KWAJ variant + /// + LZKWAJ, + + /// + /// LZ-compressed file, QBasic variant + /// + LZQBasic, + + /// + /// LZ-compressed file, SZDD variant + /// + LZSZDD, + /// /// Microsoft cabinet file /// MicrosoftCAB, - /// - /// Microsoft LZ-compressed file - /// - /// Currently has no IWrapper implementation - MicrosoftLZ, - /// /// MPQ game data archive ///