From 388567c9c639dc56715a8d673d3ca4f7a3844cd0 Mon Sep 17 00:00:00 2001
From: Deterous <138427222+Deterous@users.noreply.github.com>
Date: Mon, 6 Apr 2026 23:43:32 +0900
Subject: [PATCH 1/2] Initial XEX support (#77)
* Initial XEX support
* Fix build
* Fix reader
* Fix reader, again
* Rename field to CertificateOffset
* Update reader
* Update Printer
* Update Printer
* Parse Certificate
* More info
* Fix build
---
SabreTools.Data.Models/README.MD | 1 +
.../XenonExecutable/Certificate.cs | 110 ++++++++++
.../XenonExecutable/Constants.cs | 57 +++++
.../XenonExecutable/Enums.cs | 133 ++++++++++++
.../XenonExecutable/Executable.cs | 31 +++
.../XenonExecutable/Header.cs | 54 +++++
.../XenonExecutable/OptionalHeader.cs | 34 +++
.../XenonExecutable/TableEntry.cs | 34 +++
.../XenonExecutableTests.cs | 72 +++++++
.../XenonExecutable.cs | 200 ++++++++++++++++++
.../XenonExecutableTests.cs | 60 ++++++
SabreTools.Wrappers/WrapperFactory.cs | 11 +
SabreTools.Wrappers/WrapperType.cs | 5 +
.../XenonExecutable.Printing.cs | 124 +++++++++++
SabreTools.Wrappers/XenonExecutable.cs | 102 +++++++++
15 files changed, 1028 insertions(+)
create mode 100644 SabreTools.Data.Models/XenonExecutable/Certificate.cs
create mode 100644 SabreTools.Data.Models/XenonExecutable/Constants.cs
create mode 100644 SabreTools.Data.Models/XenonExecutable/Enums.cs
create mode 100644 SabreTools.Data.Models/XenonExecutable/Executable.cs
create mode 100644 SabreTools.Data.Models/XenonExecutable/Header.cs
create mode 100644 SabreTools.Data.Models/XenonExecutable/OptionalHeader.cs
create mode 100644 SabreTools.Data.Models/XenonExecutable/TableEntry.cs
create mode 100644 SabreTools.Serialization.Readers.Test/XenonExecutableTests.cs
create mode 100644 SabreTools.Serialization.Readers/XenonExecutable.cs
create mode 100644 SabreTools.Wrappers.Test/XenonExecutableTests.cs
create mode 100644 SabreTools.Wrappers/XenonExecutable.Printing.cs
create mode 100644 SabreTools.Wrappers/XenonExecutable.cs
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..f96252d3
--- /dev/null
+++ b/SabreTools.Serialization.Readers/XenonExecutable.cs
@@ -0,0 +1,200 @@
+using System.IO;
+using SabreTools.Data.Models.XenonExecutable;
+using SabreTools.IO.Extensions;
+using SabreTools.Matching;
+using SabreTools.Numerics.Extensions;
+using static SabreTools.Data.Models.XenonExecutable.Constants;
+
+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..6726de1f
--- /dev/null
+++ b/SabreTools.Wrappers/XenonExecutable.Printing.cs
@@ -0,0 +1,124 @@
+using System;
+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..55cacac0
--- /dev/null
+++ b/SabreTools.Wrappers/XenonExecutable.cs
@@ -0,0 +1,102 @@
+using System;
+using System.IO;
+using System.Text;
+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
+ }
+}
From adedc502e3cbe042b62160265c68897b6b5cee34 Mon Sep 17 00:00:00 2001
From: Matt Nadareski
Date: Mon, 6 Apr 2026 10:46:04 -0400
Subject: [PATCH 2/2] Editorconfig cleanup of Xenon
---
SabreTools.Serialization.Readers/XenonExecutable.cs | 7 +++----
SabreTools.Wrappers/XenonExecutable.Printing.cs | 1 -
SabreTools.Wrappers/XenonExecutable.cs | 2 --
3 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/SabreTools.Serialization.Readers/XenonExecutable.cs b/SabreTools.Serialization.Readers/XenonExecutable.cs
index f96252d3..523ec71b 100644
--- a/SabreTools.Serialization.Readers/XenonExecutable.cs
+++ b/SabreTools.Serialization.Readers/XenonExecutable.cs
@@ -3,7 +3,6 @@ using SabreTools.Data.Models.XenonExecutable;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Numerics.Extensions;
-using static SabreTools.Data.Models.XenonExecutable.Constants;
namespace SabreTools.Serialization.Readers
{
@@ -87,7 +86,7 @@ namespace SabreTools.Serialization.Readers
obj.OptionalHeaderCount = data.ReadUInt32BigEndian();
// Ensure optional headers fit within stream
- if (data.Position + 8 * obj.OptionalHeaderCount > data.Length)
+ if (data.Position + (8 * obj.OptionalHeaderCount) > data.Length)
return null;
var optionalHeaders = new OptionalHeader[obj.OptionalHeaderCount];
@@ -104,7 +103,7 @@ namespace SabreTools.Serialization.Readers
continue;
}
- uint optionalHeaderLength = (optionalHeader.HeaderID & 0xFF);
+ uint optionalHeaderLength = optionalHeader.HeaderID & 0xFF;
if (optionalHeaderLength == 0xFF)
optionalHeaderLength = 4;
else
@@ -180,7 +179,7 @@ namespace SabreTools.Serialization.Readers
obj.TableCount = data.ReadUInt32BigEndian();
// Ensure table fits within stream
- if (data.Position + 24 * obj.TableCount > data.Length)
+ if (data.Position + (24 * obj.TableCount) > data.Length)
return obj;
var table = new TableEntry[obj.TableCount];
diff --git a/SabreTools.Wrappers/XenonExecutable.Printing.cs b/SabreTools.Wrappers/XenonExecutable.Printing.cs
index 6726de1f..583c3ce3 100644
--- a/SabreTools.Wrappers/XenonExecutable.Printing.cs
+++ b/SabreTools.Wrappers/XenonExecutable.Printing.cs
@@ -1,4 +1,3 @@
-using System;
using System.Text;
using SabreTools.Data.Models.XenonExecutable;
using SabreTools.Text.Extensions;
diff --git a/SabreTools.Wrappers/XenonExecutable.cs b/SabreTools.Wrappers/XenonExecutable.cs
index 55cacac0..2235d0db 100644
--- a/SabreTools.Wrappers/XenonExecutable.cs
+++ b/SabreTools.Wrappers/XenonExecutable.cs
@@ -1,6 +1,4 @@
-using System;
using System.IO;
-using System.Text;
using SabreTools.Data.Models.XenonExecutable;
namespace SabreTools.Wrappers