From 0d4dcf4a6040b52e8ba26c1456fc0661c3a51dfa Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 26 Sep 2025 16:30:38 -0400 Subject: [PATCH] Start implementing Advanced Installer reader --- .../Readers/AdvancedInstallerTests.cs | 73 +++++++ .../Models/AdvancedInstaller/Constants.cs | 9 + .../Readers/AdvancedInstaller.cs | 180 ++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 SabreTools.Serialization.Test/Readers/AdvancedInstallerTests.cs create mode 100644 SabreTools.Serialization/Models/AdvancedInstaller/Constants.cs create mode 100644 SabreTools.Serialization/Readers/AdvancedInstaller.cs diff --git a/SabreTools.Serialization.Test/Readers/AdvancedInstallerTests.cs b/SabreTools.Serialization.Test/Readers/AdvancedInstallerTests.cs new file mode 100644 index 00000000..7d755eae --- /dev/null +++ b/SabreTools.Serialization.Test/Readers/AdvancedInstallerTests.cs @@ -0,0 +1,73 @@ +using System.IO; +using System.Linq; +using SabreTools.Serialization.Readers; +using Xunit; + +namespace SabreTools.Serialization.Test.Readers +{ + public class AdvancedInstallerTests + { + [Fact] + public void NullArray_Null() + { + byte[]? data = null; + int offset = 0; + var deserializer = new AdvancedInstaller(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void EmptyArray_Null() + { + byte[]? data = []; + int offset = 0; + var deserializer = new AdvancedInstaller(); + + 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 AdvancedInstaller(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void NullStream_Null() + { + Stream? data = null; + var deserializer = new AdvancedInstaller(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void EmptyStream_Null() + { + Stream? data = new MemoryStream([]); + var deserializer = new AdvancedInstaller(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void InvalidStream_Null() + { + Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]); + var deserializer = new AdvancedInstaller(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Models/AdvancedInstaller/Constants.cs b/SabreTools.Serialization/Models/AdvancedInstaller/Constants.cs new file mode 100644 index 00000000..27644cb2 --- /dev/null +++ b/SabreTools.Serialization/Models/AdvancedInstaller/Constants.cs @@ -0,0 +1,9 @@ +namespace SabreTools.Data.Models.AdvancedInstaller +{ + public static class Constants + { + public static readonly byte[] SignatureBytes = [0x41, 0x44, 0x56, 0x49, 0x4E, 0x53, 0x54, 0x53, 0x46, 0x58]; + + public const string SignatureString = "ADVINSTSFX"; + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Readers/AdvancedInstaller.cs b/SabreTools.Serialization/Readers/AdvancedInstaller.cs new file mode 100644 index 00000000..ccc4927d --- /dev/null +++ b/SabreTools.Serialization/Readers/AdvancedInstaller.cs @@ -0,0 +1,180 @@ +using System.IO; +using System.Text; +using SabreTools.Data.Models.AdvancedInstaller; +using SabreTools.IO.Extensions; +using static SabreTools.Data.Models.AdvancedInstaller.Constants; + +namespace SabreTools.Serialization.Readers +{ + public class AdvancedInstaller : BaseBinaryReader + { + /// + public override SFX? Deserialize(Stream? data) + { + // If the data is invalid + if (data == null || !data.CanRead) + return null; + + try + { + // Cache the current offset + long initialOffset = data.Position; + + // Create a new SFX to populate + var sfx = new SFX(); + + // Try to parse the footer from the data end + var footer = ParseFooter(data, initialOffset); + if (footer == null) + return null; + if (footer.Signature != SignatureString) + return null; + + // Set the footer + sfx.Footer = footer; + + // Seek to the entry table offset + long tableOffset = initialOffset + footer.TablePointer; + if (tableOffset < initialOffset || tableOffset >= data.Length) + return null; + + // Try to parse the entry table + var table = ParseTable(data, footer.EntryCount); + if (table.Length != footer.EntryCount) + return null; + + // Set the entry table + sfx.Entries = table; + + return sfx; + } + catch + { + // Ignore the actual error + return null; + } + } + + /// + /// Parse a Stream into a Footer + /// + /// Stream to parse + /// Initial offset to use in address comparisons + /// Filled Footer on success, null on error + public static Footer? ParseFooter(Stream data, long initialOffset) + { + // Seek to the end of the end of the data + data.Seek(0, SeekOrigin.End); + + // Cache the current offset + long endOffset = data.Position; + + // Set a maximum of 10 iterations to find the signature + int iterations = 10; + + // Search backward for the signature + bool signatureFound = false; + do + { + byte[] tempBytes = data.ReadBytes(10); + string tempStr = Encoding.ASCII.GetString(tempBytes); + if (tempStr == SignatureString) + { + signatureFound = true; + break; + } + + iterations--; + + } while (iterations > 0); + + // If no signature was found + if (!signatureFound) + return null; + + // Seek to the first footer offset field + data.Seek(-70, SeekOrigin.Current); + + // Find the actual offset of the start of the footer + uint footerStart = data.ReadUInt32LittleEndian(); + if (footerStart > endOffset) + return null; + + // If the offset is immediately prior, no name exists + bool shortFooter = footerStart == data.Position - 8; + + // Seek to the start of the footer + data.Seek(initialOffset + footerStart, SeekOrigin.Begin); + + var obj = new Footer(); + + obj.Unknown0 = data.ReadUInt32LittleEndian(); + + // TODO: Make this block safer + if (!shortFooter) + { + obj.OriginalFilenameSize = data.ReadUInt32LittleEndian(); + byte[] filenameBytes = data.ReadBytes((int)obj.OriginalFilenameSize * 2); + obj.OriginalFilename = Encoding.Unicode.GetString(filenameBytes); + obj.Unknown1 = data.ReadUInt32LittleEndian(); + } + + obj.FooterOffset = data.ReadUInt32LittleEndian(); + obj.EntryCount = data.ReadUInt32LittleEndian(); + obj.Unknown2 = data.ReadUInt32LittleEndian(); + obj.UnknownOffset = data.ReadUInt32LittleEndian(); + obj.TablePointer = data.ReadUInt32LittleEndian(); + obj.FileDataStart = data.ReadUInt32LittleEndian(); + byte[] hexStringBytes = data.ReadBytes(32); + obj.HexString = Encoding.ASCII.GetString(hexStringBytes); + obj.Unknown3 = data.ReadUInt32LittleEndian(); + byte[] signatureBytes = data.ReadBytes(10); + obj.Signature = Encoding.ASCII.GetString(signatureBytes); + // TODO: Handle padding? + + return obj; + } + + /// + /// Parse a Stream into an entry table + /// + /// Stream to parse + /// Number of entries to parse + /// Filled entry table on success, null on error + public static FileEntry[] ParseTable(Stream data, uint count) + { + if (count == 0) + return []; + + var obj = new FileEntry[count]; + for (uint i = 0; i < count; i++) + { + obj[i] = ParseFileEntry(data); + } + + return obj; + } + + /// + /// Parse a Stream into a Footer + /// + /// Stream to parse + /// Number of entries to parse + /// Filled Footer on success, null on error + public static FileEntry ParseFileEntry(Stream data) + { + var obj = new FileEntry(); + + obj.Unknown0 = data.ReadUInt32LittleEndian(); + obj.Unknown1 = data.ReadUInt32LittleEndian(); + obj.Unknown2 = data.ReadUInt32LittleEndian(); + obj.FileSize = data.ReadUInt32LittleEndian(); + obj.FileOffset = data.ReadUInt32LittleEndian(); + obj.NameSize = data.ReadUInt32LittleEndian(); + byte[] nameBytes = data.ReadBytes((int)obj.NameSize * 2); + obj.Name = Encoding.Unicode.GetString(nameBytes); + + return obj; + } + } +}