Files
2026-03-24 19:42:36 -04:00

368 lines
15 KiB
C#

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<ScriptFile>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "Wise Installer Script File";
#endregion
#region Extension Properties
/// <inheritdoc cref="ScriptHeader.DateTime"/>
public uint DateTime => Model.Header.DateTime;
/// <inheritdoc cref="ScriptHeader.Flags"/>
public ushort Flags => Model.Header.Flags;
/// <inheritdoc cref="ScriptHeader.FontSize"/>
public uint FontSize => Model.Header.FontSize;
/// <inheritdoc cref="ScriptHeader.FTPURL"/>
public string? FTPURL => Model.Header.FTPURL;
/// <inheritdoc cref="ScriptFile.HeaderStrings"/>
public string[]? HeaderStrings => Model.Header.HeaderStrings;
/// <inheritdoc cref="ScriptHeader.LanguageCount"/>
public byte LanguageCount => Model.Header.LanguageCount;
/// <inheritdoc cref="ScriptHeader.LogPathname"/>
public string? LogPathname => Model.Header.LogPathname;
/// <inheritdoc cref="ScriptHeader.MessageFont"/>
public string? MessageFont => Model.Header.MessageFont;
/// <inheritdoc cref="ScriptFile.States"/>
public MachineState[] States => Model.States;
/// <inheritdoc cref="ScriptHeader.UnknownU16_1"/>
public ushort UnknownU16_1 => Model.Header.UnknownU16_1;
/// <inheritdoc cref="ScriptHeader.UnknownU16_2"/>
public ushort UnknownU16_2 => Model.Header.UnknownU16_2;
/// <inheritdoc cref="ScriptFile.VariableLengthData"/>
public byte[]? VariableLengthData => Model.Header.VariableLengthData;
#endregion
#region Constructors
/// <inheritdoc/>
public WiseScript(ScriptFile model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public WiseScript(ScriptFile model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public WiseScript(ScriptFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public WiseScript(ScriptFile model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public WiseScript(ScriptFile model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public WiseScript(ScriptFile model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a Wise installer script file from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the script</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>A Wise installer script file wrapper on success, null on failure</returns>
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);
}
/// <summary>
/// Create a Wise installer script file from a Stream
/// </summary>
/// <param name="data">Stream representing the script</param>
/// <returns>A Wise installer script file wrapper on success, null on failure</returns>
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
/// <summary>
/// Process the state machine and perform all required actions
/// </summary>
/// <param name="header">Overlay header used for reference</param>
/// <param name="sourceDirectory">Directory where installer files live, if possible</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if there were no errors during processing, false otherwise</returns>
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<string, string> 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;
}
/// <summary>
/// Attempt to write INI data to the correct file
/// </summary>
/// <param name="obj">INI information</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="index">File index for automatic naming</param>
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
}
}