diff --git a/SabreTools.Data.Models/README.MD b/SabreTools.Data.Models/README.MD index 99843c18..bcd9fb33 100644 --- a/SabreTools.Data.Models/README.MD +++ b/SabreTools.Data.Models/README.MD @@ -88,6 +88,7 @@ Below is a list of all existing namespaces with the `SabreTools.Data.Models` pre | `Xbox` | Xbox and Xbox 360 file formats | | `XboxExecutable` | Xbox Executable (XBE) | | `XDVDFS` | Xbox DVD Filesystem (XISO) | +| `XenonExecutable` | Xenon (Xbox 360) Executable (XEX) | | `XZ` | xz archive | | `XZP` | XBox Package File | | `ZArchive` | ZArchive (ZAR) | diff --git a/SabreTools.Data.Models/XenonExecutable/Certificate.cs b/SabreTools.Data.Models/XenonExecutable/Certificate.cs new file mode 100644 index 00000000..6a33e829 --- /dev/null +++ b/SabreTools.Data.Models/XenonExecutable/Certificate.cs @@ -0,0 +1,110 @@ +namespace SabreTools.Data.Models.XenonExecutable +{ + /// + /// Xenon (Xbox 360) Executable format certificate structure + /// + /// + /// + public class Certificate + { + /// + /// Length of the certificate structure in bytes + /// + /// Big-endian + public uint Length { get; set; } + + /// + /// Size of the entire image in bytes + /// Uncompressed size (not the raw XEX file size) + /// + /// Big-endian + public uint ImageSize { get; set; } + + /// + /// Public signature for XEX file + /// Signed by Microsoft for XEX authenticity + /// + public byte[] Signature { get; set; } = new byte[256]; + + /// + /// Base File Load Address, retail discs always(?) 0x00000174 + /// + /// Big-endian + public uint BaseFileLoadAddress { get; set; } + + /// + /// Image flags, see Constants.ImageFlags + /// + /// Big-endian + public uint ImageFlags { get; set; } + + /// + /// Base address for the image to be placed in memory + /// Known values: 0x82000000 (retail games), 0x92000000 (applications) + /// + /// Big-endian + public uint ImageBaseAddress { get; set; } + + /// + /// Unknown hash, likely SHA-1 integrity hash for portion of image + /// + public byte[] UnknownHash1 { get; set; } = new byte[20]; + + /// + /// Unknown field + /// Known values: 0x00000002 (retail games, some applications), 0x00000003 (applications) + /// + /// Big-endian + public uint Unknown0128 { get; set; } + + /// + /// Unknown hash, likely SHA-1 integrity hash for portion of image + /// + public byte[] UnknownHash2 { get; set; } = new byte[20]; + + /// + /// Full Media ID, probably a GUID + /// Last (LSB) four bytes in hexadecimal are the unique ringcode of the disc being pressed + /// + public byte[] MediaID { get; set; } = new byte[16]; + + /// + /// XEX File Key + /// + public byte[] XEXFileKey { get; set; } = new byte[16]; + + /// + /// Unknown field, often zeroed + /// + /// Big-endian + public uint Unknown0160 { get; set; } + + /// + /// Unknown hash, likely SHA-1 integrity hash for portion of image + /// + public byte[] UnknownHash3 { get; set; } = new byte[20]; + + /// + /// Flags for console region locking, known values in Constants.RegionFlags + /// + /// Big-endian + public uint RegionFlags { get; set; } + + /// + /// Allowed media type flags, see Constants.AllowedMediaTypeFlags + /// + /// Big-endian + public uint AllowedMediaTypeFlags { get; set; } + + /// + /// Number of entries in the following table + /// + /// Big-endian + public uint TableCount { get; set; } + + /// + /// Table, 24-bytes per entry + /// + public TableEntry[] Table { get; set; } = []; + } +} diff --git a/SabreTools.Data.Models/XenonExecutable/Constants.cs b/SabreTools.Data.Models/XenonExecutable/Constants.cs new file mode 100644 index 00000000..8d4f66cb --- /dev/null +++ b/SabreTools.Data.Models/XenonExecutable/Constants.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; + +namespace SabreTools.Data.Models.XenonExecutable +{ + /// + public static class Constants + { + /// + /// Xenon (Xbox 360) Executable magic number ("XEX2") + /// + public static readonly byte[] MagicBytes = [0x58, 0x45, 0x58, 0x32]; + + /// + /// Xenon (Xbox 360) Executable magic number ("XEX2") + /// + public const string MagicString = "XEX2"; + + /// + /// Xenon (Xbox 360) Optional Header types + /// + public static readonly Dictionary OptionalHeaderTypes = new() + { + [0x0002FF] = "Resource Info", + [0x0003FF] = "Base File Format", // Compression and Encryption details + [0x000405] = "Base Reference", + [0x004304] = "Disc Profile ID", + [0x0005FF] = "Delta Patch Descriptor", + [0x0080FF] = "Bounding Path", + [0x008105] = "Device ID", + [0x010001] = "Original Base Address", + [0x010100] = "Entry Point", + [0x010201] = "Image Base Address", + [0x0103FF] = "Import Libraries", + [0x018002] = "Checksum/Timestamp", // Last 4 bytes are XEX Timestamp + [0x018102] = "Enabled For Callcap", + [0x018200] = "Enabled For Fastcap", + [0x0183FF] = "Original PE Name", + [0x0200FF] = "Static Libraries", + [0x020104] = "TLS Info", + [0x020200] = "Default Stack Size", + [0x020301] = "Default Filesystem Cache Size", + [0x020401] = "Default Heap Size", + [0x028002] = "Page Heap Size and Flags", + [0x030000] = "System Flags", // Constants.SystemFlags + [0x040006] = "Execution ID", + [0x0401FF] = "Service ID List", + [0x040201] = "Title Workspace Size", + [0x040310] = "Game Ratings", + [0x040404] = "LAN Key", + [0x0405FF] = "Xbox 360 Logo", + [0x0406FF] = "Multidisc Media IDs", + [0x0407FF] = "Alternate Title IDs", + [0x040801] = "Additional Title Memory", + [0xE10402] = "Exports by Name" + }; + } +} diff --git a/SabreTools.Data.Models/XenonExecutable/Enums.cs b/SabreTools.Data.Models/XenonExecutable/Enums.cs new file mode 100644 index 00000000..6cf2b37f --- /dev/null +++ b/SabreTools.Data.Models/XenonExecutable/Enums.cs @@ -0,0 +1,133 @@ +using System; + +namespace SabreTools.Data.Models.XenonExecutable +{ + /// + /// Flags for Xbox 360 Module Flags field + /// + [Flags] + public enum ModuleFlags : uint + { + TITLE_MODULE = 0x00000001, + EXPORTS_TO_TITLE = 0x00000002, + SYSTEM_DEBUGGER = 0x00000004, + DLL_MODULE = 0x00000008, + MODULE_PATCH = 0x00000010, + FULL_PATCH = 0x00000020, + DELTA_PATCH = 0x00000040, + USER_MODE = 0x00000080, + UNKNOWN_MASK = 0xFFFFFF00, + } + + /// + /// Flags for Certificate ImageFlags field + /// + [Flags] + public enum ImageFlags : uint + { + MANUFACTURING_UTILITY = 0x00000002, + MANUFACTURING_SUPPORT_TOOL = 0x00000004, + ORIGINAL_MEDIA_ONLY = 0x00000008, + CARDEA_KEY_WMDRM_ND = 0x00000100, + XEIKA_KEY_AP25 = 0x00000200, + TITLE_USERMODE = 0x00000400, + SYSTEM_USERMODE = 0x00000800, + ORANGE0 = 0x00001000, + ORANGE1 = 0x00002000, + ORANGE2 = 0x00004000, + SIGNED_KEYVAULT_RESTRICTED = 0x00008000, + IPTV_SIGNUP_APPLICATION = 0x00010000, + IPTV_TITLE_APPLICATION = 0x00020000, + KEYVAULT_PRIVILEGES_REQUIRED = 0x04000000, + APPLICATION_REQUIRED = 0x08000000, + FOUR_KB_PAGES = 0x10000000, // else 64 KB Pages + NO_GAME_REGION = 0x20000000, + REVOCATION_CHECK_OPTIONAL = 0x40000000, + REVOCATION_CHECK_REQUIRED = 0x80000000, + UNKNOWN_MASK = 0x03FC80F1, + } + + /// + /// Flags for OptionalHeader SystemFlags field + /// + [Flags] + public enum SystemFlags : uint + { + NO_FORCED_REBOOT = 0x00000001, + FOREGROUND_TASKS = 0x00000002, + NO_ODD_MAPPING = 0x00000004, + HANDLES_MCE_INPUT = 0x00000008, + RESTRICTED_HUD_FEATURES = 0x00000010, + HANDLES_GAMEPAD_DISCONNECT = 0x00000020, + HAS_SECURE_SOCKETS = 0x00000040, + XBOX1_INTEROPERABILITY = 0x00000080, + DASH_CONTEXT = 0x00000100, + USES_GAME_VOICE_CHANNEL = 0x00000200, + PAL50_INCOMPATIBLE = 0x00000400, + INSECURE_UTILITY_DRIVE_SUPPORT = 0x00000800, + XAM_HOOKS = 0x00001000, + ACCESSES_PII = 0x00002000, + CROSS_PLATFORM_SYSTEM_LINK = 0x00004000, + MULTIDISC_SWAP = 0x00008000, + SUPPORTS_INSECURE_MULTIDISC_MEDIA = 0x00010000, + ANTIPIRACY25_MEDIA = 0x00020000, + NO_CONFIRM_EXIT = 0x00040000, + ALLOW_BACKGROUND_DOWNLOADING = 0x00080000, + CREATE_PERSISTABLE_RAM_DRIVE = 0x00100000, + INHERIT_PERSISTENT_RAM_DRIVE = 0x00200000, + ALLOW_HUD_VIBRATION = 0x00400000, + ALLOW_BOTH_UTILITY_PARTITIONS_ACCESS = 0x00800000, + HANDLES_IPTV_INPUT = 0x01000000, + PREFERS_BIG_BUTTON_INPUT = 0x02000000, + ALLOW_EXTENDED_SYSTEM_RESERVATION = 0x04000000, + MULTIDISC_CROSS_TITLE = 0x08000000, + TITLE_INSTALL_INCOMPATIBLE = 0x10000000, + ALLOW_AVATAR_GET_METADATA_BY_XUID = 0x20000000, + ALLOW_CONTROLLER_SWAPPING = 0x40000000, + DASH_EXTENSIBILITY_MODULE = 0x80000000, + } + + /// + /// Flags for Certificate AllowedMediaTypeFlags field + /// + [Flags] + public enum AllowedMediaTypeFlags : uint + { + HARD_DISK = 0x00000001, + XBOX1_ORIGINAL_DISK = 0x00000002, + DVD_CD = 0x00000004, + DVD_5 = 0x00000008, + DVD_9 = 0x00000010, + SYSTEM_FLASH = 0x00000020, + MEMORY_UNIT = 0x00000080, + USB_MASS_STORAGE_DEVICE = 0x00000100, + NETWORKED_SMB_SHARE = 0x00000200, + DIRECT_FROM_RAM = 0x00000400, + RAM_DRIVE = 0x00000800, + INSECURE_PACKAGE = 0x01000000, + SAVEGAME_PACKAGE = 0x02000000, + LOCALLY_SIGNED_PACKAGE = 0x04000000, + LIVE_SIGNED_PACKAGE = 0x08008000, + XBOX_PACKAGE = 0x10000000, + UNKNOWN_MASK = 0xE0FFE040, + } + + /// + /// Flags for Xbox 360 console regions + /// Source: Original Research + /// + [Flags] + public enum RegionFlags : uint + { + NTSC_U = 0x00_00_00_FF, + JAPAN = 0x00_00_01_00, + CHINA = 0x00_00_02_00, + ASIA = 0x00_00_F8_00, + JAPAN_AND_ASIA = 0x00_00_F9_00, + NTSC_J_EXCLUDING_CHINA = 0x00_00_FD_00, + OCEANIA = 0x00_01_00_00, + EUROPE = 0x00_FE_00_00, + PAL = 0x00_FF_00_00, + REGION_FREE = 0xFF_FF_FF_FF, + } +} diff --git a/SabreTools.Data.Models/XenonExecutable/Executable.cs b/SabreTools.Data.Models/XenonExecutable/Executable.cs new file mode 100644 index 00000000..9f08bb90 --- /dev/null +++ b/SabreTools.Data.Models/XenonExecutable/Executable.cs @@ -0,0 +1,31 @@ +namespace SabreTools.Data.Models.XenonExecutable +{ + /// + /// Xenon (Xbox 360) Executable format (XEX2) + /// It is based on PE format and is PPC architecutre (therefore Big-Endian) + /// During alpha stage, Xenon was a slightly modified Apple Power Mac G5 + /// Early (Before March 2005) builds used pure PE-formatted executables, June 2005 XDK began requiring XEX-format + /// Early (August 2005 and earlier) XEX-format images (XEX0, XEX?, XEX-, XEX1) are not supported. + /// + /// + /// + public class Executable + { + /// + /// XEX header + /// + public Header Header { get; set; } = new(); + + /// + /// XEX certificate structure + /// + public Certificate Certificate { get; set; } = new(); + + /// + /// PE data, too large to be read into memory + /// Encrypted and/or compressed blob on most XEX files + /// Occassionally padded with zeroes at the end + /// + public byte[]? CompressedData { get; set; } + } +} diff --git a/SabreTools.Data.Models/XenonExecutable/Header.cs b/SabreTools.Data.Models/XenonExecutable/Header.cs new file mode 100644 index 00000000..92d0c345 --- /dev/null +++ b/SabreTools.Data.Models/XenonExecutable/Header.cs @@ -0,0 +1,54 @@ +namespace SabreTools.Data.Models.XenonExecutable +{ + /// + /// Xenon (Xbox 360) Executable format header + /// + /// + /// + public class Header + { + /// + /// "XEX2" is the only supported magic identifier string + /// "XEX0", "XEX-", "XEX?", and "XEX1" files are not supported, and are only found in early pre-production builds + /// + public byte[] MagicNumber { get; set; } = new byte[4]; + + /// + /// Bit field with lowest 8 bits having known values: (lowest to highest) + /// Title Module, Exports To Title, System Debugger, DLL Module, Module Patch, Patch Full, Patch Delta, User Mode + /// Upper 3 bytes should be zeroed + /// + /// Big-endian + public uint ModuleFlags { get; set; } + + /// + /// Address at which the PE data begins + /// The PE data is encrypted/compressed in retail XEX files + /// + /// Big-endian + public uint PEDataOffset { get; set; } + + /// + /// Reserved field, should be zeroed + /// + /// Big-endian + public uint Reserved { get; set; } + + /// + /// Address at which the certificate structure begins + /// + /// Big-endian + public uint CertificateOffset { get; set; } + + /// + /// Number of optional headers + /// + /// Big-endian + public uint OptionalHeaderCount { get; set; } + + /// + /// Optional headers that follow the main header + /// + public OptionalHeader[]? OptionalHeaders { get; set; } + } +} diff --git a/SabreTools.Data.Models/XenonExecutable/OptionalHeader.cs b/SabreTools.Data.Models/XenonExecutable/OptionalHeader.cs new file mode 100644 index 00000000..9e213f1f --- /dev/null +++ b/SabreTools.Data.Models/XenonExecutable/OptionalHeader.cs @@ -0,0 +1,34 @@ +namespace SabreTools.Data.Models.XenonExecutable +{ + /// + /// Xenon (Xbox 360) Executable format optional header + /// This is a flexible metadata list of data types and either their value or a pointer to their value + /// + /// + /// + public class OptionalHeader + { + /// + /// Header type identifier + /// Known ID values are stored in Constants.OptionalHeaderTypes + /// + /// Big-endian + public uint HeaderID { get; set; } + + /// + /// If lowest byte of HeaderID is 0x00/0x01, then HeaderOffset is the data itself + /// If the lowest byte is 0xFF, then HeaderOffset is the base offset into XEX file + /// and the first DWORD read is the total size to read + /// Otherwise, the lowest byte is the number of uint to read from HeaderOffset + /// + /// Big-endian + public uint HeaderOffset { get; set; } + + /// + /// If HeaderData is a data offset, then HeaderDataBytes is variable-length data it points to + /// The meaning and structure of these bytes is dependent on the HeaderID value + /// If HeaderData is the data itself, then this field is null + /// + public byte[]? HeaderData { get; set; } + } +} diff --git a/SabreTools.Data.Models/XenonExecutable/TableEntry.cs b/SabreTools.Data.Models/XenonExecutable/TableEntry.cs new file mode 100644 index 00000000..b7f98bb1 --- /dev/null +++ b/SabreTools.Data.Models/XenonExecutable/TableEntry.cs @@ -0,0 +1,34 @@ +namespace SabreTools.Data.Models.XenonExecutable +{ + /// + /// Xenon (Xbox 360) Executable format certificate table + /// + /// + /// + public class TableEntry + { + /// + /// Table entry ID + /// Known values: + /// 0x00000011, 0x00000012, 0x00000013 (Retail games) + /// 0x00000101, 0x00000102, 0x00000103 (Applications) + /// Unique IDs from XboxMCX-V.XEX (Windows Vista): + /// 0x00000023, 0x000000E1 (Once, one after another) + /// 0x00000031, 0x000000D2 (Once, one after another) + /// 0x00000072, 0x00000093 (Once, one after another) + /// 0x00000063 (Once, final entry) + /// Unique IDs from XboxMCX-V.XEX (Windows 7): + /// 0x000000D3, 0x00000031 (Once, one after another) + /// 0x000000A1, 0x00000062 (Once, one after another) + /// 0x00000072, 0x00000093 (Once, one after another) + /// 0x00000023 (Once, final entry) + /// + /// Big-endian + public uint ID { get; set; } = new(); + + /// + /// Table entry data, 20 bytes + /// + public byte[]? Data { get; set; } = new byte[20]; + } +} diff --git a/SabreTools.Serialization.Readers.Test/XenonExecutableTests.cs b/SabreTools.Serialization.Readers.Test/XenonExecutableTests.cs new file mode 100644 index 00000000..370ac59e --- /dev/null +++ b/SabreTools.Serialization.Readers.Test/XenonExecutableTests.cs @@ -0,0 +1,72 @@ +using System.IO; +using System.Linq; +using Xunit; + +namespace SabreTools.Serialization.Readers.Test +{ + public class XenonExecutableTests + { + [Fact] + public void NullArray_Null() + { + byte[]? data = null; + int offset = 0; + var deserializer = new XenonExecutable(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void EmptyArray_Null() + { + byte[]? data = []; + int offset = 0; + var deserializer = new XenonExecutable(); + + 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 XenonExecutable(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void NullStream_Null() + { + Stream? data = null; + var deserializer = new XenonExecutable(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void EmptyStream_Null() + { + Stream? data = new MemoryStream([]); + var deserializer = new XenonExecutable(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void InvalidStream_Null() + { + Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]); + var deserializer = new XenonExecutable(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + } +} diff --git a/SabreTools.Serialization.Readers/XenonExecutable.cs b/SabreTools.Serialization.Readers/XenonExecutable.cs new file mode 100644 index 00000000..523ec71b --- /dev/null +++ b/SabreTools.Serialization.Readers/XenonExecutable.cs @@ -0,0 +1,199 @@ +using System.IO; +using SabreTools.Data.Models.XenonExecutable; +using SabreTools.IO.Extensions; +using SabreTools.Matching; +using SabreTools.Numerics.Extensions; + +namespace SabreTools.Serialization.Readers +{ + public class XenonExecutable : BaseBinaryReader + { + /// + public override Executable? Deserialize(Stream? data) + { + // If the data is invalid + if (data is null || !data.CanRead) + return null; + + try + { + // Cache the current offset + long initialOffset = data.Position; + + // Create a new executable to fill + var xex = new Executable(); + + #region ParseHeader + + // Parse the file header + var header = ParseHeader(data, initialOffset); + if (header is null) + return null; + + // Set the XEX header + xex.Header = header; + + #endregion + + #region ParseCertificate + + // Get the certificate address + long certificateOffset = initialOffset + header.CertificateOffset; + if (certificateOffset >= initialOffset && certificateOffset < data.Length) + { + // Seek to the certificate + data.SeekIfPossible(certificateOffset, SeekOrigin.Begin); + + // Parse the certificate + var certificate = ParseCertificate(data); + if (certificate is null) + return null; + + // Set the XEX certificate + xex.Certificate = certificate; + } + + #endregion + + return xex; + } + catch + { + // Ignore the actual error + return null; + } + } + + /// + /// Parse a Stream into an XEX Header + /// + /// Stream to parse + /// Filled XEX Header on success, null on error + public static Header? ParseHeader(Stream data, long initialOffset) + { + var obj = new Header(); + + var magicNumber = data.ReadBytes(4); + if (!magicNumber.EqualsExactly(Constants.MagicBytes)) + return null; + + obj.MagicNumber = magicNumber; + + obj.ModuleFlags = data.ReadUInt32BigEndian(); + obj.PEDataOffset = data.ReadUInt32BigEndian(); + obj.Reserved = data.ReadUInt32BigEndian(); + obj.CertificateOffset = data.ReadUInt32BigEndian(); + obj.OptionalHeaderCount = data.ReadUInt32BigEndian(); + + // Ensure optional headers fit within stream + if (data.Position + (8 * obj.OptionalHeaderCount) > data.Length) + return null; + + var optionalHeaders = new OptionalHeader[obj.OptionalHeaderCount]; + for (int i = 0; i < obj.OptionalHeaderCount; i++) + { + var optionalHeader = new OptionalHeader(); + optionalHeader.HeaderID = data.ReadUInt32BigEndian(); + optionalHeader.HeaderOffset = data.ReadUInt32BigEndian(); + + // If HeaderID LSB is 0x00 or 0x01, HeaderOffset is the data itself + if ((optionalHeader.HeaderID & 0xFE) == 0x00) + { + optionalHeaders[i] = optionalHeader; + continue; + } + + uint optionalHeaderLength = optionalHeader.HeaderID & 0xFF; + if (optionalHeaderLength == 0xFF) + optionalHeaderLength = 4; + else + optionalHeaderLength *= 4; + + // Ignore invalid offset + if (optionalHeader.HeaderOffset < initialOffset || optionalHeader.HeaderOffset + optionalHeaderLength > data.Length) + { + optionalHeaders[i] = optionalHeader; + continue; + } + + // Read the optional header data + long currentPosition = data.Position; + data.SeekIfPossible(optionalHeader.HeaderOffset, SeekOrigin.Begin); + + // Deal with variable length optional header data + if ((optionalHeader.HeaderID & 0xFF) == 0xFF) + { + var length = data.ReadUInt32BigEndian(); + optionalHeaderLength = length - 4; + + // Ignore invalid length + if (optionalHeaderLength <= 0 || data.Position + optionalHeaderLength > data.Length) + { + data.SeekIfPossible(currentPosition, SeekOrigin.Begin); + optionalHeaders[i] = optionalHeader; + continue; + } + } + + // Save the optional header data in model + optionalHeader.HeaderData = data.ReadBytes((int)optionalHeaderLength); + optionalHeaders[i] = optionalHeader; + + // Return to position in header + data.SeekIfPossible(currentPosition, SeekOrigin.Begin); + } + + obj.OptionalHeaders = optionalHeaders; + + return obj; + } + + /// + /// Parse a Stream into an XEX Certificate + /// + /// Stream to parse + /// Filled XEX Certificate on success, null on error + public static Certificate? ParseCertificate(Stream data) + { + var obj = new Certificate(); + + // Ensure certificate fits within stream + if (data.Position + 388 > data.Length) + return null; + + obj.Length = data.ReadUInt32BigEndian(); + obj.ImageSize = data.ReadUInt32BigEndian(); + obj.Signature = data.ReadBytes(256); + obj.BaseFileLoadAddress = data.ReadUInt32BigEndian(); + obj.ImageFlags = data.ReadUInt32BigEndian(); + obj.ImageBaseAddress = data.ReadUInt32BigEndian(); + obj.UnknownHash1 = data.ReadBytes(20); + obj.Unknown0128 = data.ReadUInt32BigEndian(); + obj.UnknownHash2 = data.ReadBytes(20); + obj.MediaID = data.ReadBytes(16); + obj.XEXFileKey = data.ReadBytes(16); + obj.Unknown0160 = data.ReadUInt32BigEndian(); + obj.UnknownHash3 = data.ReadBytes(20); + obj.RegionFlags = data.ReadUInt32BigEndian(); + obj.AllowedMediaTypeFlags = data.ReadUInt32BigEndian(); + obj.TableCount = data.ReadUInt32BigEndian(); + + // Ensure table fits within stream + if (data.Position + (24 * obj.TableCount) > data.Length) + return obj; + + var table = new TableEntry[obj.TableCount]; + for (int i = 0; i < obj.TableCount; i++) + { + var row = new TableEntry(); + row.ID = data.ReadUInt32BigEndian(); + row.Data = data.ReadBytes(20); + table[i] = row; + } + + obj.Table = table; + + return obj; + } + } +} diff --git a/SabreTools.Wrappers.Test/XenonExecutableTests.cs b/SabreTools.Wrappers.Test/XenonExecutableTests.cs new file mode 100644 index 00000000..09fddc08 --- /dev/null +++ b/SabreTools.Wrappers.Test/XenonExecutableTests.cs @@ -0,0 +1,60 @@ +using System.IO; +using System.Linq; +using Xunit; + +namespace SabreTools.Wrappers.Test +{ + public class XenonExecutableTests + { + [Fact] + public void NullArray_Null() + { + byte[]? data = null; + int offset = 0; + var actual = XenonExecutable.Create(data, offset); + Assert.Null(actual); + } + + [Fact] + public void EmptyArray_Null() + { + byte[]? data = []; + int offset = 0; + var actual = XenonExecutable.Create(data, offset); + Assert.Null(actual); + } + + [Fact] + public void InvalidArray_Null() + { + byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)]; + int offset = 0; + var actual = XenonExecutable.Create(data, offset); + Assert.Null(actual); + } + + [Fact] + public void NullStream_Null() + { + Stream? data = null; + var actual = XenonExecutable.Create(data); + Assert.Null(actual); + } + + [Fact] + public void EmptyStream_Null() + { + Stream? data = new MemoryStream([]); + var actual = XenonExecutable.Create(data); + Assert.Null(actual); + } + + [Fact] + public void InvalidStream_Null() + { + Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]); + var actual = XenonExecutable.Create(data); + Assert.Null(actual); + } + } +} diff --git a/SabreTools.Wrappers/WrapperFactory.cs b/SabreTools.Wrappers/WrapperFactory.cs index db904c03..20d848d1 100644 --- a/SabreTools.Wrappers/WrapperFactory.cs +++ b/SabreTools.Wrappers/WrapperFactory.cs @@ -69,6 +69,7 @@ namespace SabreTools.Wrappers WrapperType.WiseScript => WiseScript.Create(data), WrapperType.XboxExecutable => XboxExecutable.Create(data), WrapperType.XDVDFS => XDVDFS.Create(data), + WrapperType.XenonExecutable => XenonExecutable.Create(data), WrapperType.XZ => XZ.Create(data), WrapperType.XZP => XZP.Create(data), WrapperType.ZArchive => ZArchive.Create(data), @@ -939,6 +940,16 @@ namespace SabreTools.Wrappers #endregion + #region XenonExecutable + + if (magic.StartsWith(Data.Models.XenonExecutable.Constants.MagicBytes)) + return WrapperType.XenonExecutable; + + if (extension.Equals("xex", StringComparison.OrdinalIgnoreCase)) + return WrapperType.XenonExecutable; + + #endregion + #region XZ if (magic.StartsWith(Data.Models.XZ.Constants.HeaderSignatureBytes)) diff --git a/SabreTools.Wrappers/WrapperType.cs b/SabreTools.Wrappers/WrapperType.cs index 0c32eebf..05868981 100644 --- a/SabreTools.Wrappers/WrapperType.cs +++ b/SabreTools.Wrappers/WrapperType.cs @@ -293,6 +293,11 @@ namespace SabreTools.Wrappers /// XDVDFS, + /// + /// Xenon (Xbox 360) Executable + /// + XenonExecutable, + /// /// xz archive /// diff --git a/SabreTools.Wrappers/XenonExecutable.Printing.cs b/SabreTools.Wrappers/XenonExecutable.Printing.cs new file mode 100644 index 00000000..583c3ce3 --- /dev/null +++ b/SabreTools.Wrappers/XenonExecutable.Printing.cs @@ -0,0 +1,123 @@ +using System.Text; +using SabreTools.Data.Models.XenonExecutable; +using SabreTools.Text.Extensions; + +namespace SabreTools.Wrappers +{ + public partial class XenonExecutable : IPrintable + { +#if NETCOREAPP + /// + public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions); +#endif + + /// + public void PrintInformation(StringBuilder builder) + { + builder.AppendLine("Xenon (Xbox 360) Executable Information:"); + builder.AppendLine("-------------------------"); + builder.AppendLine(); + + Print(builder, Model.Header); + Print(builder, Model.Header.OptionalHeaders); + Print(builder, Model.Certificate); + } + + 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.ModuleFlags, " Module Flags"); + builder.AppendLine(header.PEDataOffset, " PE Data Offset"); + builder.AppendLine(header.Reserved, " Reserved"); + builder.AppendLine(header.CertificateOffset, " Certificate Offset"); + builder.AppendLine(header.OptionalHeaderCount, " Optional Header Count"); + + builder.AppendLine(); + } + + private static void Print(StringBuilder builder, OptionalHeader[]? optionalHeaders) + { + builder.AppendLine(" Optional Headers:"); + builder.AppendLine(" -------------------------"); + if (optionalHeaders is null) + { + builder.AppendLine(" No optional headers"); + builder.AppendLine(); + return; + } + + foreach (var optionalHeader in optionalHeaders) + { + builder.AppendLine(optionalHeader.HeaderID, " Header ID"); + if (Constants.OptionalHeaderTypes.TryGetValue(optionalHeader.HeaderID, out string? headerType)) + builder.AppendLine(headerType, " Header Type (Parsed)"); + else + builder.AppendLine("[UNKNOWN]", " Header Type (Parsed)"); + + if (optionalHeader.HeaderData is not null) + { + builder.AppendLine(optionalHeader.HeaderOffset, " Header Offset"); + builder.AppendLine(optionalHeader.HeaderData, " Header Data"); + } + else + { + builder.AppendLine(optionalHeader.HeaderOffset, " Header Data"); + } + + builder.AppendLine(); + } + + builder.AppendLine(); + } + + 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.Length, " Length"); + builder.AppendLine(certificate.Signature, " Signature"); + builder.AppendLine(certificate.BaseFileLoadAddress, " Base File Load Address"); + builder.AppendLine(certificate.ImageFlags, " Image Flags"); + builder.AppendLine(certificate.ImageBaseAddress, " Image Base Address"); + builder.AppendLine(certificate.UnknownHash1, " Unknown Hash 1"); + builder.AppendLine(certificate.Unknown0128, " Unknown0128"); + builder.AppendLine(certificate.UnknownHash2, " Unknown Hash 2"); + builder.AppendLine(certificate.MediaID, " Media ID"); + builder.AppendLine(certificate.XEXFileKey, " XEX File Key"); + builder.AppendLine(certificate.Unknown0160, " Unknown0160"); + builder.AppendLine(certificate.UnknownHash3, " Unknown Hash 3"); + builder.AppendLine(certificate.RegionFlags, " Region Flags"); + builder.AppendLine(certificate.AllowedMediaTypeFlags, " Allowed Media Type Flags"); + builder.AppendLine(certificate.TableCount, " Certificate Table Count"); + + if (certificate.Table.Length == 0) + builder.AppendLine(" Empty Certificate Table"); + else + builder.AppendLine(" Certificate Table:"); + + for (int i = 0; i < certificate.Table.Length; i++) + { + builder.AppendLine(certificate.Table[i].ID, $" Entry {i} ID"); + builder.AppendLine(certificate.Table[i].Data, $" Entry {i} Data"); + } + + builder.AppendLine(); + } + } +} diff --git a/SabreTools.Wrappers/XenonExecutable.cs b/SabreTools.Wrappers/XenonExecutable.cs new file mode 100644 index 00000000..2235d0db --- /dev/null +++ b/SabreTools.Wrappers/XenonExecutable.cs @@ -0,0 +1,100 @@ +using System.IO; +using SabreTools.Data.Models.XenonExecutable; + +namespace SabreTools.Wrappers +{ + public partial class XenonExecutable : WrapperBase + { + #region Descriptive Properties + + /// + public override string DescriptionString => "Xenon (Xbox 360) Executable"; + + #endregion + + #region Extension Properties + + /// + public Header Header => Model.Header; + + /// + public Certificate Certificate => Model.Certificate; + + #endregion + + #region Constructors + + /// + public XenonExecutable(Executable model, byte[] data) : base(model, data) { } + + /// + public XenonExecutable(Executable model, byte[] data, int offset) : base(model, data, offset) { } + + /// + public XenonExecutable(Executable model, byte[] data, int offset, int length) : base(model, data, offset, length) { } + + /// + public XenonExecutable(Executable model, Stream data) : base(model, data) { } + + /// + public XenonExecutable(Executable model, Stream data, long offset) : base(model, data, offset) { } + + /// + public XenonExecutable(Executable model, Stream data, long offset, long length) : base(model, data, offset, length) { } + + #endregion + + #region Static Constructors + + /// + /// Create a Xenon (Xbox 360) Executable from a byte array and offset + /// + /// Byte array representing the archive + /// Offset within the array to parse + /// A Xenon (Xbox 360) Executable wrapper on success, null on failure + public static XenonExecutable? 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 Xenon (Xbox 360) Executable from a Stream + /// + /// Stream representing the archive + /// A Xenon (Xbox 360) Executable wrapper on success, null on failure + public static XenonExecutable? 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.XenonExecutable().Deserialize(data); + if (model is null) + return null; + + return new XenonExecutable(model, data, currentOffset); + } + catch + { + return null; + } + } + + #endregion + } +}