using System; using System.Collections.Generic; using System.IO; using System.Text; using SabreTools.Data.Models.WiseInstaller; using SabreTools.Data.Models.WiseInstaller.Actions; using SabreTools.Numerics.Extensions; namespace SabreTools.Wrappers { public partial class WiseScript : WrapperBase { #region Descriptive Properties /// public override string DescriptionString => "Wise Installer Script File"; #endregion #region Extension Properties /// public uint DateTime => Model.Header.DateTime; /// public ushort Flags => Model.Header.Flags; /// public uint FontSize => Model.Header.FontSize; /// public string? FTPURL => Model.Header.FTPURL; /// public string[]? HeaderStrings => Model.Header.HeaderStrings; /// public byte LanguageCount => Model.Header.LanguageCount; /// public string? LogPathname => Model.Header.LogPathname; /// public string? MessageFont => Model.Header.MessageFont; /// public MachineState[] States => Model.States; /// public ushort UnknownU16_1 => Model.Header.UnknownU16_1; /// public ushort UnknownU16_2 => Model.Header.UnknownU16_2; /// public byte[]? VariableLengthData => Model.Header.VariableLengthData; #endregion #region Constructors /// public WiseScript(ScriptFile model, byte[] data) : base(model, data) { } /// public WiseScript(ScriptFile model, byte[] data, int offset) : base(model, data, offset) { } /// public WiseScript(ScriptFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { } /// public WiseScript(ScriptFile model, Stream data) : base(model, data) { } /// public WiseScript(ScriptFile model, Stream data, long offset) : base(model, data, offset) { } /// public WiseScript(ScriptFile model, Stream data, long offset, long length) : base(model, data, offset, length) { } #endregion #region Static Constructors /// /// Create a Wise installer script file from a byte array and offset /// /// Byte array representing the script /// Offset within the array to parse /// A Wise installer script file wrapper on success, null on failure public static WiseScript? Create(byte[]? data, int offset) { // If the data is invalid if (data is 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 Wise installer script file from a Stream /// /// Stream representing the script /// A Wise installer script file wrapper on success, null on failure public static WiseScript? Create(Stream? data) { // If the data is invalid if (data is null || !data.CanRead) return null; try { // Cache the current offset long currentOffset = data.Position; var model = new Serialization.Readers.WiseScript().Deserialize(data); if (model is null) return null; return new WiseScript(model, data, currentOffset); } catch { return null; } } #endregion #region Processing /// /// Process the state machine and perform all required actions /// /// Overlay header used for reference /// Directory where installer files live, if possible /// Output directory to write to /// True to include debug data, false otherwise /// True if there were no errors during processing, false otherwise public bool ProcessStateMachine(WiseOverlayHeader header, string? sourceDirectory, string outputDirectory, bool includeDebug) { // If the state machine is invalid if (States.Length == 0) return false; // Initialize important loop information int normalFileCount = 0; Dictionary environment = []; if (sourceDirectory is not null) environment.Add("INST", sourceDirectory); // Loop through the state machine and process foreach (var state in States) { #pragma warning disable IDE0010 switch (state.Op) { case OperationCode.InstallFile: if (state.Data is not InstallFile fileHeader) return false; // Try to extract to the output directory header.ExtractFile(fileHeader, ++normalFileCount, outputDirectory, includeDebug); break; case OperationCode.EditIniFile: if (state.Data is not EditIniFile editIniFile) return false; // Try to write to the output directory WriteIniData(editIniFile, outputDirectory, ++normalFileCount); break; case OperationCode.DisplayBillboard: if (state.Data is not DisplayBillboard displayBillboard) return false; // Try to extract to the output directory header.ExtractFile(displayBillboard, outputDirectory, includeDebug); break; case OperationCode.DeleteFile: if (state.Data is not DeleteFile deleteFile) return false; if (includeDebug) Console.WriteLine($"File {deleteFile.Pathname} is supposed to be deleted"); break; case OperationCode.CreateDirectory: if (state.Data is not CreateDirectory createDirectory) return false; if (createDirectory.Pathname is null) return false; try { if (includeDebug) Console.WriteLine($"Directory {createDirectory.Pathname} is being created"); // Ensure directory separators are consistent string newDirectoryName = Path.Combine(outputDirectory, createDirectory.Pathname); if (Path.DirectorySeparatorChar == '\\') newDirectoryName = newDirectoryName.Replace('/', '\\'); else if (Path.DirectorySeparatorChar == '/') newDirectoryName = newDirectoryName.Replace('\\', '/'); // Perform path replacements foreach (var kvp in environment) { newDirectoryName = newDirectoryName.Replace($"%{kvp.Key}%", kvp.Value); } newDirectoryName = newDirectoryName.Replace("%", string.Empty); // Remove wildcards from end of the path if (newDirectoryName.EndsWith("*.*")) #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER newDirectoryName = newDirectoryName[..^4]; #else newDirectoryName = newDirectoryName.Substring(0, newDirectoryName.Length - 4); #endif Directory.CreateDirectory(newDirectoryName); } catch { if (includeDebug) Console.WriteLine($"Directory {createDirectory.Pathname} could not be created!"); } break; case OperationCode.CopyLocalFile: if (state.Data is not CopyLocalFile copyLocalFile) return false; if (copyLocalFile.Source is null) return false; if (copyLocalFile.Destination is null) return false; try { if (includeDebug) Console.WriteLine($"File {copyLocalFile.Source} is being copied to {copyLocalFile.Destination}"); // Ensure directory separators are consistent string oldFilePath = copyLocalFile.Source; if (Path.DirectorySeparatorChar == '\\') oldFilePath = oldFilePath.Replace('/', '\\'); else if (Path.DirectorySeparatorChar == '/') oldFilePath = oldFilePath.Replace('\\', '/'); // Perform path replacements foreach (var kvp in environment) { oldFilePath = oldFilePath.Replace($"%{kvp.Key}%", kvp.Value); } oldFilePath = oldFilePath.Replace("%", string.Empty); // Sanity check if (!File.Exists(oldFilePath)) { if (includeDebug) Console.WriteLine($"File {copyLocalFile.Source} is supposed to be copied to {copyLocalFile.Destination}, but it does not exist!"); break; } // Ensure directory separators are consistent string newFilePath = Path.Combine(outputDirectory, copyLocalFile.Destination); if (Path.DirectorySeparatorChar == '\\') newFilePath = newFilePath.Replace('/', '\\'); else if (Path.DirectorySeparatorChar == '/') newFilePath = newFilePath.Replace('\\', '/'); // Perform path replacements foreach (var kvp in environment) { newFilePath = newFilePath.Replace($"%{kvp.Key}%", kvp.Value); } newFilePath = newFilePath.Replace("%", string.Empty); // Sanity check string? newFileDirectory = Path.GetDirectoryName(newFilePath); if (newFileDirectory is not null && !Directory.Exists(newFileDirectory)) Directory.CreateDirectory(newFileDirectory); File.Copy(oldFilePath, newFilePath); } catch { if (includeDebug) Console.WriteLine($"File {copyLocalFile.Source} could not be copied!"); } break; case OperationCode.CustomDialogSet: if (state.Data is not CustomDialogSet customDialogSet) return false; // Try to extract to the output directory ++normalFileCount; header.ExtractFile(customDialogSet, outputDirectory, includeDebug); break; case OperationCode.GetTemporaryFilename: if (state.Data is not GetTemporaryFilename getTemporaryFilename) return false; if (getTemporaryFilename.Variable is not null) environment[getTemporaryFilename.Variable] = Guid.NewGuid().ToString(); break; case OperationCode.AddTextToInstallLog: if (state.Data is not AddTextToInstallLog addTextToInstallLog) return false; if (includeDebug) Console.WriteLine($"INSTALL.LOG: {addTextToInstallLog.Text}"); break; default: break; } #pragma warning restore IDE0010 } return true; } /// /// Attempt to write INI data to the correct file /// /// INI information /// Output directory to write to /// File index for automatic naming private static void WriteIniData(EditIniFile obj, string outputDirectory, int index) { // Ensure directory separators are consistent string iniFilePath = obj.Pathname ?? $"WISE{index:X4}.ini"; if (Path.DirectorySeparatorChar == '\\') iniFilePath = iniFilePath.Replace('/', '\\'); else if (Path.DirectorySeparatorChar == '/') iniFilePath = iniFilePath.Replace('\\', '/'); // Ignore path replacements iniFilePath = iniFilePath.Replace("%", string.Empty); // Ensure the full output directory exists iniFilePath = Path.Combine(outputDirectory, iniFilePath); var directoryName = Path.GetDirectoryName(iniFilePath); if (directoryName is not null && !Directory.Exists(directoryName)) Directory.CreateDirectory(directoryName); using var iniFile = File.Open(iniFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); iniFile.Write(Encoding.ASCII.GetBytes($"[{obj.Section}]\n")); iniFile.Write(Encoding.ASCII.GetBytes($"{obj.Values ?? string.Empty}\n")); iniFile.Flush(); } #endregion } }