Merge branch 'main' into attempt-metadata-overhaul

This commit is contained in:
Matt Nadareski
2026-04-06 10:46:53 -04:00
15 changed files with 1024 additions and 0 deletions

View File

@@ -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) |

View File

@@ -0,0 +1,110 @@
namespace SabreTools.Data.Models.XenonExecutable
{
/// <summary>
/// Xenon (Xbox 360) Executable format certificate structure
/// </summary>
/// <see href="http://oskarsapps.mine.nu/xexdump"/>
/// <see href="https://free60.org/System-Software/Formats/XEX/"/>
public class Certificate
{
/// <summary>
/// Length of the certificate structure in bytes
/// </summary>
/// <remarks>Big-endian</remarks>
public uint Length { get; set; }
/// <summary>
/// Size of the entire image in bytes
/// Uncompressed size (not the raw XEX file size)
/// </summary>
/// <remarks>Big-endian</remarks>
public uint ImageSize { get; set; }
/// <summary>
/// Public signature for XEX file
/// Signed by Microsoft for XEX authenticity
/// </summary>
public byte[] Signature { get; set; } = new byte[256];
/// <summary>
/// Base File Load Address, retail discs always(?) 0x00000174
/// </summary>
/// <remarks>Big-endian</remarks>
public uint BaseFileLoadAddress { get; set; }
/// <summary>
/// Image flags, see Constants.ImageFlags
/// </summary>
/// <remarks>Big-endian</remarks>
public uint ImageFlags { get; set; }
/// <summary>
/// Base address for the image to be placed in memory
/// Known values: 0x82000000 (retail games), 0x92000000 (applications)
/// </summary>
/// <remarks>Big-endian</remarks>
public uint ImageBaseAddress { get; set; }
/// <summary>
/// Unknown hash, likely SHA-1 integrity hash for portion of image
/// </summary>
public byte[] UnknownHash1 { get; set; } = new byte[20];
/// <summary>
/// Unknown field
/// Known values: 0x00000002 (retail games, some applications), 0x00000003 (applications)
/// </summary>
/// <remarks>Big-endian</remarks>
public uint Unknown0128 { get; set; }
/// <summary>
/// Unknown hash, likely SHA-1 integrity hash for portion of image
/// </summary>
public byte[] UnknownHash2 { get; set; } = new byte[20];
/// <summary>
/// Full Media ID, probably a GUID
/// Last (LSB) four bytes in hexadecimal are the unique ringcode of the disc being pressed
/// </summary>
public byte[] MediaID { get; set; } = new byte[16];
/// <summary>
/// XEX File Key
/// </summary>
public byte[] XEXFileKey { get; set; } = new byte[16];
/// <summary>
/// Unknown field, often zeroed
/// </summary>
/// <remarks>Big-endian</remarks>
public uint Unknown0160 { get; set; }
/// <summary>
/// Unknown hash, likely SHA-1 integrity hash for portion of image
/// </summary>
public byte[] UnknownHash3 { get; set; } = new byte[20];
/// <summary>
/// Flags for console region locking, known values in Constants.RegionFlags
/// </summary>
/// <remarks>Big-endian</remarks>
public uint RegionFlags { get; set; }
/// <summary>
/// Allowed media type flags, see Constants.AllowedMediaTypeFlags
/// </summary>
/// <remarks>Big-endian</remarks>
public uint AllowedMediaTypeFlags { get; set; }
/// <summary>
/// Number of entries in the following table
/// </summary>
/// <remarks>Big-endian</remarks>
public uint TableCount { get; set; }
/// <summary>
/// Table, 24-bytes per entry
/// </summary>
public TableEntry[] Table { get; set; } = [];
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.XenonExecutable
{
/// <see href="https://free60.org/System-Software/Formats/XEX/"/>
public static class Constants
{
/// <summary>
/// Xenon (Xbox 360) Executable magic number ("XEX2")
/// </summary>
public static readonly byte[] MagicBytes = [0x58, 0x45, 0x58, 0x32];
/// <summary>
/// Xenon (Xbox 360) Executable magic number ("XEX2")
/// </summary>
public const string MagicString = "XEX2";
/// <summary>
/// Xenon (Xbox 360) Optional Header types
/// </summary>
public static readonly Dictionary<uint, string> 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"
};
}
}

View File

@@ -0,0 +1,133 @@
using System;
namespace SabreTools.Data.Models.XenonExecutable
{
/// <summary>
/// Flags for Xbox 360 Module Flags field
/// </summary>
[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,
}
/// <summary>
/// Flags for Certificate ImageFlags field
/// </summary>
[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,
}
/// <summary>
/// Flags for OptionalHeader SystemFlags field
/// </summary>
[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,
}
/// <summary>
/// Flags for Certificate AllowedMediaTypeFlags field
/// </summary>
[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,
}
/// <summary>
/// Flags for Xbox 360 console regions
/// Source: Original Research
/// </summary>
[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,
}
}

View File

@@ -0,0 +1,31 @@
namespace SabreTools.Data.Models.XenonExecutable
{
/// <summary>
/// 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.
/// </summary>
/// <see href="http://oskarsapps.mine.nu/xexdump"/>
/// <see href="https://free60.org/System-Software/Formats/XEX/"/>
public class Executable
{
/// <summary>
/// XEX header
/// </summary>
public Header Header { get; set; } = new();
/// <summary>
/// XEX certificate structure
/// </summary>
public Certificate Certificate { get; set; } = new();
/// <summary>
/// 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
/// </summary>
public byte[]? CompressedData { get; set; }
}
}

View File

@@ -0,0 +1,54 @@
namespace SabreTools.Data.Models.XenonExecutable
{
/// <summary>
/// Xenon (Xbox 360) Executable format header
/// </summary>
/// <see href="http://oskarsapps.mine.nu/xexdump"/>
/// <see href="https://free60.org/System-Software/Formats/XEX/"/>
public class Header
{
/// <summary>
/// "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
/// </summary>
public byte[] MagicNumber { get; set; } = new byte[4];
/// <summary>
/// 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
/// </summary>
/// <remarks>Big-endian</remarks>
public uint ModuleFlags { get; set; }
/// <summary>
/// Address at which the PE data begins
/// The PE data is encrypted/compressed in retail XEX files
/// </summary>
/// <remarks>Big-endian</remarks>
public uint PEDataOffset { get; set; }
/// <summary>
/// Reserved field, should be zeroed
/// </summary>
/// <remarks>Big-endian</remarks>
public uint Reserved { get; set; }
/// <summary>
/// Address at which the certificate structure begins
/// </summary>
/// <remarks>Big-endian</remarks>
public uint CertificateOffset { get; set; }
/// <summary>
/// Number of optional headers
/// </summary>
/// <remarks>Big-endian</remarks>
public uint OptionalHeaderCount { get; set; }
/// <summary>
/// Optional headers that follow the main header
/// </summary>
public OptionalHeader[]? OptionalHeaders { get; set; }
}
}

View File

@@ -0,0 +1,34 @@
namespace SabreTools.Data.Models.XenonExecutable
{
/// <summary>
/// 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
/// </summary>
/// <see href="http://oskarsapps.mine.nu/xexdump"/>
/// <see href="https://free60.org/System-Software/Formats/XEX/"/>
public class OptionalHeader
{
/// <summary>
/// Header type identifier
/// Known ID values are stored in Constants.OptionalHeaderTypes
/// </summary>
/// <remarks>Big-endian</remarks>
public uint HeaderID { get; set; }
/// <summary>
/// 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
/// </summary>
/// <remarks>Big-endian</remarks>
public uint HeaderOffset { get; set; }
/// <summary>
/// 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
/// </summary>
public byte[]? HeaderData { get; set; }
}
}

View File

@@ -0,0 +1,34 @@
namespace SabreTools.Data.Models.XenonExecutable
{
/// <summary>
/// Xenon (Xbox 360) Executable format certificate table
/// </summary>
/// <see href="http://oskarsapps.mine.nu/xexdump"/>
/// <see href="https://free60.org/System-Software/Formats/XEX/"/>
public class TableEntry
{
/// <summary>
/// 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)
/// </summary>
/// <remarks>Big-endian</remarks>
public uint ID { get; set; } = new();
/// <summary>
/// Table entry data, 20 bytes
/// </summary>
public byte[]? Data { get; set; } = new byte[20];
}
}

View File

@@ -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<byte>(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<byte>(0xFF, 1024)]);
var deserializer = new XenonExecutable();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
}
}

View File

@@ -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<Executable>
{
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Parse a Stream into an XEX Header
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled XEX Header on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into an XEX Certificate
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled XEX Certificate on success, null on error</returns>
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;
}
}
}

View File

@@ -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<byte>(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<byte>(0xFF, 1024)]);
var actual = XenonExecutable.Create(data);
Assert.Null(actual);
}
}
}

View File

@@ -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))

View File

@@ -293,6 +293,11 @@ namespace SabreTools.Wrappers
/// </summary>
XDVDFS,
/// <summary>
/// Xenon (Xbox 360) Executable
/// </summary>
XenonExecutable,
/// <summary>
/// xz archive
/// </summary>

View File

@@ -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
/// <inheritdoc/>
public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
#endif
/// <inheritdoc/>
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();
}
}
}

View File

@@ -0,0 +1,100 @@
using System.IO;
using SabreTools.Data.Models.XenonExecutable;
namespace SabreTools.Wrappers
{
public partial class XenonExecutable : WrapperBase<Executable>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "Xenon (Xbox 360) Executable";
#endregion
#region Extension Properties
/// <inheritdoc cref="Header"/>
public Header Header => Model.Header;
/// <inheritdoc cref="Certificate"/>
public Certificate Certificate => Model.Certificate;
#endregion
#region Constructors
/// <inheritdoc/>
public XenonExecutable(Executable model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public XenonExecutable(Executable model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public XenonExecutable(Executable model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public XenonExecutable(Executable model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public XenonExecutable(Executable model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public XenonExecutable(Executable model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a Xenon (Xbox 360) Executable from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the archive</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>A Xenon (Xbox 360) Executable wrapper on success, null on failure</returns>
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);
}
/// <summary>
/// Create a Xenon (Xbox 360) Executable from a Stream
/// </summary>
/// <param name="data">Stream representing the archive</param>
/// <returns>A Xenon (Xbox 360) Executable wrapper on success, null on failure</returns>
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
}
}