diff --git a/SabreTools.Serialization.Test/Wrappers/XboxExecutableTests.cs b/SabreTools.Serialization.Test/Wrappers/XboxExecutableTests.cs new file mode 100644 index 00000000..5aa3b499 --- /dev/null +++ b/SabreTools.Serialization.Test/Wrappers/XboxExecutableTests.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.Linq; +using SabreTools.Serialization.Wrappers; +using Xunit; + +namespace SabreTools.Serialization.Test.Wrappers +{ + public class XboxExecutableTests + { + [Fact] + public void NullArray_Null() + { + byte[]? data = null; + int offset = 0; + var actual = XboxExecutable.Create(data, offset); + Assert.Null(actual); + } + + [Fact] + public void EmptyArray_Null() + { + byte[]? data = []; + int offset = 0; + var actual = XboxExecutable.Create(data, offset); + Assert.Null(actual); + } + + [Fact] + public void InvalidArray_Null() + { + byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)]; + int offset = 0; + var actual = XboxExecutable.Create(data, offset); + Assert.Null(actual); + } + + [Fact] + public void NullStream_Null() + { + Stream? data = null; + var actual = XboxExecutable.Create(data); + Assert.Null(actual); + } + + [Fact] + public void EmptyStream_Null() + { + Stream? data = new MemoryStream([]); + var actual = XboxExecutable.Create(data); + Assert.Null(actual); + } + + [Fact] + public void InvalidStream_Null() + { + Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]); + var actual = XboxExecutable.Create(data); + Assert.Null(actual); + } + } +} diff --git a/SabreTools.Serialization/Extensions/StringBuilderExtensions.cs b/SabreTools.Serialization/Extensions/StringBuilderExtensions.cs index 4d3d0cc3..a29f7833 100644 --- a/SabreTools.Serialization/Extensions/StringBuilderExtensions.cs +++ b/SabreTools.Serialization/Extensions/StringBuilderExtensions.cs @@ -275,6 +275,24 @@ namespace SabreTools.Data.Extensions return sb.AppendLine($"{prefixString}: {valueString}"); } + /// + /// Append a line containing a UInt8[][] value to a StringBuilder + /// + public static StringBuilder AppendLine(this StringBuilder sb, byte[][]? value, string prefixString) + { + string valueString = "[NULL]"; + if (value is not null) + { + var valueArr = Array.ConvertAll(value, ba => ba is null ? "[NULL]" : BitConverter.ToString(ba).Replace('-', ' ')); + valueString = string.Join(", ", valueArr); + } + + if (valueString.Length == 0) + return sb.AppendLine($"{prefixString}: [EMPTY]"); + + return sb.AppendLine($"{prefixString}: {valueString}"); + } + /// /// Append a line containing a Char[] value to a StringBuilder /// diff --git a/SabreTools.Serialization/Models/XboxExecutable/Executable.cs b/SabreTools.Serialization/Models/XboxExecutable/Executable.cs index a83a9b58..3ff180b1 100644 --- a/SabreTools.Serialization/Models/XboxExecutable/Executable.cs +++ b/SabreTools.Serialization/Models/XboxExecutable/Executable.cs @@ -14,37 +14,31 @@ namespace SabreTools.Data.Models.XboxExecutable /// /// Certificate structure pointed to by /// - /// TODO: Determine if address is real or virtual public Certificate? Certificate { get; set; } /// /// Section headers pointed to by /// - /// TODO: Determine if address is real or virtual public SectionHeader[] SectionHeaders { get; set; } = []; /// /// Thread Local Storage (TLS) structure pointed to by /// - /// TODO: Determine if address is real or virtual public ThreadLocalStorage? ThreadLocalStorage { get; set; } /// /// Library versions pointed to by /// - /// TODO: Determine if address is real or virtual public LibraryVersion[] LibraryVersions { get; set; } = []; /// /// Kernel library version pointed to by /// - /// TODO: Determine if address is real or virtual public LibraryVersion? KernelLibraryVersion { get; set; } /// /// XAPI library version pointed to by /// - /// TODO: Determine if address is real or virtual public LibraryVersion? XAPILibraryVersion { get; set; } } } diff --git a/SabreTools.Serialization/WrapperFactory.cs b/SabreTools.Serialization/WrapperFactory.cs index 1d0e36ba..748ecb63 100644 --- a/SabreTools.Serialization/WrapperFactory.cs +++ b/SabreTools.Serialization/WrapperFactory.cs @@ -66,6 +66,7 @@ namespace SabreTools.Serialization WrapperType.VBSP => VBSP.Create(data), WrapperType.VPK => VPK.Create(data), WrapperType.WAD => WAD3.Create(data), + WrapperType.XboxExecutable => XboxExecutable.Create(data), WrapperType.XZ => XZ.Create(data), WrapperType.XZP => XZP.Create(data), WrapperType.ZSTD => ZSTD.Create(data), @@ -913,6 +914,16 @@ namespace SabreTools.Serialization #endregion + #region XboxExecutable + + if (magic.StartsWith(Data.Models.XboxExecutable.Constants.MagicBytes)) + return WrapperType.XboxExecutable; + + if (extension.Equals("xbe", StringComparison.OrdinalIgnoreCase)) + return WrapperType.XboxExecutable; + + #endregion + #region XZ if (magic.StartsWith(Data.Models.XZ.Constants.HeaderSignatureBytes)) diff --git a/SabreTools.Serialization/Wrappers/WrapperType.cs b/SabreTools.Serialization/Wrappers/WrapperType.cs index 802a483f..5de19f42 100644 --- a/SabreTools.Serialization/Wrappers/WrapperType.cs +++ b/SabreTools.Serialization/Wrappers/WrapperType.cs @@ -283,6 +283,11 @@ namespace SabreTools.Serialization.Wrappers /// WiseScript, + /// + /// XBox Executable + /// + XboxExecutable, + /// /// xz archive /// diff --git a/SabreTools.Serialization/Wrappers/XboxExecutable.Printing.cs b/SabreTools.Serialization/Wrappers/XboxExecutable.Printing.cs new file mode 100644 index 00000000..d10b6707 --- /dev/null +++ b/SabreTools.Serialization/Wrappers/XboxExecutable.Printing.cs @@ -0,0 +1,194 @@ +using System.Text; +using SabreTools.Data.Extensions; +using SabreTools.Data.Models.XboxExecutable; + +namespace SabreTools.Serialization.Wrappers +{ + public partial class XboxExecutable : IPrintable + { +#if NETCOREAPP + /// + public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions); +#endif + + /// + public void PrintInformation(StringBuilder builder) + { + builder.AppendLine("XBox Executable Information:"); + builder.AppendLine("-------------------------"); + builder.AppendLine(); + + Print(builder, Model.Header); + Print(builder, Model.Certificate); + Print(builder, Model.SectionHeaders); + Print(builder, Model.ThreadLocalStorage); + Print(builder, Model.LibraryVersions); + Print(builder, Model.KernelLibraryVersion, "Kernel ", 2); + Print(builder, Model.XAPILibraryVersion, "XAPI ", 2); + } + + private static void Print(StringBuilder builder, Certificate? certificate) + { + builder.AppendLine(" Certificate Information:"); + builder.AppendLine(" -------------------------"); + if (certificate is null) + { + builder.AppendLine(" No certificate"); + builder.AppendLine(); + return; + } + + builder.AppendLine(certificate.SizeOfCertificate, " Size of certificate"); + builder.AppendLine(certificate.TimeDate, " Time/Date stamp"); + builder.AppendLine(certificate.TitleID, " Title ID"); + builder.AppendLine(certificate.TitleName, " Title name"); + builder.AppendLine(Encoding.Unicode.GetString(certificate.TitleName), " Title name (Unicode)"); + builder.AppendLine(certificate.AlternativeTitleIDs, " Alternative title IDs"); + builder.AppendLine($" Allowed media types: {certificate.AllowedMediaTypes} (0x{certificate.AllowedMediaTypes:X})"); + builder.AppendLine($" Game region: {certificate.GameRegion} (0x{certificate.GameRegion:X})"); + builder.AppendLine(certificate.GameRatings, " Game ratings"); + builder.AppendLine(certificate.DiskNumber, " Disk number"); + builder.AppendLine(certificate.Version, " Version"); + builder.AppendLine(certificate.LANKey, " LAN key"); + builder.AppendLine(certificate.SignatureKey, " Signature key"); + builder.AppendLine(certificate.AlternateSignatureKeys, " Alternate signature keys"); + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, Header? header) + { + builder.AppendLine(" Header Information:"); + builder.AppendLine(" -------------------------"); + if (header is null) + { + builder.AppendLine(" No header"); + builder.AppendLine(); + return; + } + + builder.AppendLine(header.MagicNumber, " Magic number"); + builder.AppendLine(header.DigitalSignature, " Digital signature"); + builder.AppendLine(header.BaseAddress, " Base address"); + builder.AppendLine(header.SizeOfHeaders, " Size of headers"); + builder.AppendLine(header.SizeOfImage, " Size of image"); + builder.AppendLine(header.SizeOfImageHeader, " Size of image header"); + builder.AppendLine(header.TimeDate, " Time/Date stamp"); + builder.AppendLine(header.CertificateAddress, " Certificate address"); + builder.AppendLine(header.NumberOfSections, " Number of sections"); + builder.AppendLine(header.SectionHeadersAddress, " Section headers address"); + builder.AppendLine($" Initialization flags: {header.InitializationFlags} (0x{header.InitializationFlags:X})"); + builder.AppendLine(header.EntryPoint, " Entry point"); + builder.AppendLine(header.TLSAddress, " TLS address"); + builder.AppendLine(header.PEStackCommit, " PE stack commit"); + builder.AppendLine(header.PEHeapReserve, " PE heap reserve"); + builder.AppendLine(header.PEHeapCommit, " PE heap commit"); + builder.AppendLine(header.PEBaseAddress, " PE base address"); + builder.AppendLine(header.PESizeOfImage, " PE size of image"); + builder.AppendLine(header.PEChecksum, " PE checksum"); + builder.AppendLine(header.PETimeDate, " PE time/date stamp"); + builder.AppendLine(header.DebugPathNameAddress, " Debug path name address"); + builder.AppendLine(header.DebugFileNameAddress, " Debug file name address"); + builder.AppendLine(header.DebugUnicodeFileNameAddress, " Debug Unicode file name address"); + builder.AppendLine(header.KernelImageThunkAddress, " Kernel image thunk address"); + builder.AppendLine(header.NonKernelImportDirectoryAddress, " Non-kernel import directory address"); + builder.AppendLine(header.NumberOfLibraryVersions, " Number of library versions"); + builder.AppendLine(header.LibraryVersionsAddress, " Library versions address"); + builder.AppendLine(header.KernelLibraryVersionAddress, " Kernel library version address"); + builder.AppendLine(header.XAPILibraryVersionAddress, " XAPI library version address"); + builder.AppendLine(header.LogoBitmapAddress, " Logo bitmap address"); + builder.AppendLine(header.LogoBitmapSize, " Logo bitmap size"); + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, LibraryVersion? libraryVersion, string prefix, int padding) + { + builder.AppendLine($"{"".PadLeft(padding)}{prefix}Library Version Information:"); + builder.AppendLine($"{"".PadLeft(padding)}-------------------------"); + if (libraryVersion is null) + { + builder.AppendLine($"{"".PadLeft(padding)}No library version"); + builder.AppendLine(); + return; + } + + builder.AppendLine(libraryVersion.LibraryName, $"{"".PadLeft(padding)}Library name"); + builder.AppendLine(Encoding.ASCII.GetString(libraryVersion.LibraryName), $"{"".PadLeft(padding)}Library name (ASCII)"); + builder.AppendLine(libraryVersion.MajorVersion, $"{"".PadLeft(padding)}Major version"); + builder.AppendLine(libraryVersion.MinorVersion, $"{"".PadLeft(padding)}Minor version"); + builder.AppendLine(libraryVersion.BuildVersion, $"{"".PadLeft(padding)}Build version"); + builder.AppendLine($"{"".PadLeft(padding)}Library flags: {libraryVersion.LibraryFlags} (0x{libraryVersion.LibraryFlags:X})"); + builder.AppendLine(); + } + + private void Print(StringBuilder builder, LibraryVersion[] entries) + { + builder.AppendLine(" Library Version Table Information:"); + builder.AppendLine(" -------------------------"); + if (entries is null || entries.Length == 0) + { + builder.AppendLine(" No library version table items"); + builder.AppendLine(); + return; + } + + for (int i = 0; i < entries!.Length; i++) + { + var entry = entries[i]; + Print(builder, entry, string.Empty, 4); + } + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, SectionHeader[] entries) + { + builder.AppendLine(" Section Table Information:"); + builder.AppendLine(" -------------------------"); + if (entries is null || entries.Length == 0) + { + builder.AppendLine(" No section table items"); + builder.AppendLine(); + return; + } + + for (int i = 0; i < entries!.Length; i++) + { + var entry = entries[i]; + + builder.AppendLine($" Section Table Entry {i}"); + builder.AppendLine($" Allowed media types: {entry.SectionFlags} (0x{entry.SectionFlags:X})"); + builder.AppendLine(entry.VirtualAddress, " Virtual address"); + builder.AppendLine(entry.VirtualSize, " Virtual size"); + builder.AppendLine(entry.RawAddress, " Raw address"); + builder.AppendLine(entry.RawSize, " Raw size"); + builder.AppendLine(entry.SectionNameAddress, " Section name address"); + builder.AppendLine(entry.SectionNameReferenceCount, " Section name reference count"); + builder.AppendLine(entry.HeadSharedPageReferenceCountAddress, " Head shared page reference count address"); + builder.AppendLine(entry.TailSharedPageReferenceCountAddress, " Tail shared page reference count address"); + builder.AppendLine(entry.SectionDigest, " Section digest"); + } + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, ThreadLocalStorage? tls) + { + builder.AppendLine(" Thread-Local Storage (TLS) Information:"); + builder.AppendLine(" -------------------------"); + if (tls is null) + { + builder.AppendLine(" No thread-local storage"); + builder.AppendLine(); + return; + } + + builder.AppendLine(tls.DataStartAddress, " Data start address"); + builder.AppendLine(tls.DataEndAddress, " Data end address"); + builder.AppendLine(tls.TLSIndexAddress, " TLS index address"); + builder.AppendLine(tls.TLSCallbackAddress, " TLS callback address"); + builder.AppendLine(tls.SizeOfZeroFill, " Size of zero fill"); + builder.AppendLine(tls.Characteristics, " Characteristics"); + builder.AppendLine(); + } + } +} diff --git a/SabreTools.Serialization/Wrappers/XboxExecutable.cs b/SabreTools.Serialization/Wrappers/XboxExecutable.cs new file mode 100644 index 00000000..612fd58f --- /dev/null +++ b/SabreTools.Serialization/Wrappers/XboxExecutable.cs @@ -0,0 +1,118 @@ +using System.IO; +using SabreTools.Data.Models.XboxExecutable; + +namespace SabreTools.Serialization.Wrappers +{ + public partial class XboxExecutable : WrapperBase + { + #region Descriptive Properties + + /// + public override string DescriptionString => "XBox Executable"; + + #endregion + + #region Extension Properties + + /// + public uint BaseAddress => Model.Header?.BaseAddress ?? 0; + + /// + public Certificate? Certificate => Model.Certificate; + + /// + public byte[] DigitalSignature => Model.Header?.DigitalSignature ?? []; + + /// + public LibraryVersion? KernelLibraryVersion => Model.KernelLibraryVersion; + + /// + public LibraryVersion[] LibraryVersions => Model.LibraryVersions; + + /// + public SectionHeader[] SectionHeaders => Model.SectionHeaders; + + /// + public ThreadLocalStorage? ThreadLocalStorage => Model.ThreadLocalStorage; + + /// + public LibraryVersion? XAPILibraryVersion => Model.XAPILibraryVersion; + + #endregion + + #region Constructors + + /// + public XboxExecutable(Executable model, byte[] data) : base(model, data) { } + + /// + public XboxExecutable(Executable model, byte[] data, int offset) : base(model, data, offset) { } + + /// + public XboxExecutable(Executable model, byte[] data, int offset, int length) : base(model, data, offset, length) { } + + /// + public XboxExecutable(Executable model, Stream data) : base(model, data) { } + + /// + public XboxExecutable(Executable model, Stream data, long offset) : base(model, data, offset) { } + + /// + public XboxExecutable(Executable model, Stream data, long offset, long length) : base(model, data, offset, length) { } + + #endregion + + #region Static Constructors + + /// + /// Create an XBox Executable from a byte array and offset + /// + /// Byte array representing the archive + /// Offset within the array to parse + /// An XBox Executable wrapper on success, null on failure + public static XboxExecutable? 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 an XBox Executable from a Stream + /// + /// Stream representing the archive + /// An XBox Executable wrapper on success, null on failure + public static XboxExecutable? 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 Readers.XboxExecutable().Deserialize(data); + if (model is null) + return null; + + return new XboxExecutable(model, data, currentOffset); + } + catch + { + return null; + } + } + + #endregion + } +}