├── XboxToolkitTest ├── default.xex ├── libXexUnpack.dll ├── XboxToolkitTest.csproj └── DirectoryStructureTest.cs ├── native ├── osx │ └── libXexUnpack.dylib ├── linux-x64 │ └── libXexUnpack.so └── win-x64 │ └── libXexUnpack.dll ├── XboxToolkit ├── ISOFormat.cs ├── ContainerType.cs ├── Internal │ ├── Models │ │ ├── CCIIndex.cs │ │ ├── ISODetail.cs │ │ ├── TreeNodeInfo.cs │ │ ├── CCIDetail.cs │ │ ├── GodDetails.cs │ │ ├── XgdHeader.cs │ │ ├── GodStructs.cs │ │ └── XexStructs.cs │ ├── ContainerBuilder │ │ ├── FileEntry.cs │ │ ├── DirectoryEntry.cs │ │ ├── SectorAllocator.cs │ │ └── ContainerBuilderHelper.cs │ ├── Xbe │ │ ├── XbeTls.cs │ │ ├── XbeLibraryVersion.cs │ │ ├── XbeSectionHeader.cs │ │ └── XbeHheader.cs │ ├── StructUtility.cs │ ├── Constants.cs │ ├── Decoders │ │ ├── ISOSectorDecoder.cs │ │ ├── GodSectorDecoder.cs │ │ └── CCISectorDecoder.cs │ ├── Helpers.cs │ └── XexUnpack.cs ├── XexRegion.cs ├── XgdInfo.cs ├── Models │ ├── Genre.cs │ ├── Xbe │ │ ├── XbeSummary.cs │ │ └── XbeCertificate.cs │ ├── Dds │ │ ├── DdsPixelFormat.cs │ │ └── DdsHeader.cs │ ├── GenreType.cs │ ├── MarketplaceMetaData.cs │ └── XexMetaData.cs ├── ProcessingOptions.cs ├── Interface │ ├── ISectorDecoder.cs │ ├── IContainerReader.cs │ ├── SectorDecoder.cs │ └── ContainerReader.cs ├── UnicodeHelper.cs ├── XboxToolkit.csproj ├── ISOContainerReader.cs ├── BiosLogoUtility.cs ├── GODContainerReader.cs ├── CCIContainerReader.cs ├── XbeUtility.cs ├── MarketplaceUtility.cs ├── XprUtility.cs └── XexUtility.cs ├── XboxToolkit.sln ├── .gitignore └── README.md /XboxToolkitTest/default.xex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Resurgent/XboxToolkit/HEAD/XboxToolkitTest/default.xex -------------------------------------------------------------------------------- /native/osx/libXexUnpack.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Resurgent/XboxToolkit/HEAD/native/osx/libXexUnpack.dylib -------------------------------------------------------------------------------- /XboxToolkitTest/libXexUnpack.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Resurgent/XboxToolkit/HEAD/XboxToolkitTest/libXexUnpack.dll -------------------------------------------------------------------------------- /native/linux-x64/libXexUnpack.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Resurgent/XboxToolkit/HEAD/native/linux-x64/libXexUnpack.so -------------------------------------------------------------------------------- /native/win-x64/libXexUnpack.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Resurgent/XboxToolkit/HEAD/native/win-x64/libXexUnpack.dll -------------------------------------------------------------------------------- /XboxToolkit/ISOFormat.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit 2 | { 3 | public enum ISOFormat 4 | { 5 | XboxOriginal, 6 | Xbox360 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /XboxToolkit/ContainerType.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit 2 | { 3 | public enum ContainerType 4 | { 5 | Unknown, 6 | XboxOriginal, 7 | Xbox360 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Models/CCIIndex.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit.Internal.Models 2 | { 3 | internal struct CCIIndex 4 | { 5 | public ulong Value; 6 | 7 | public bool LZ4Compressed; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /XboxToolkit/XexRegion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XboxToolkit 4 | { 5 | [Flags] 6 | public enum XexRegion 7 | { 8 | Unknown = 0, 9 | USA = 1, 10 | Japan = 2, 11 | Europe = 4, 12 | Global = 7 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Models/ISODetail.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace XboxToolkit.Internal.Models 4 | { 5 | internal struct ISODetail 6 | { 7 | public Stream Stream; 8 | public long StartSector; 9 | public long EndSector; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /XboxToolkit/XgdInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XboxToolkit 4 | { 5 | public class XgdInfo 6 | { 7 | public uint BaseSector; 8 | public uint RootDirSector; 9 | public uint RootDirSize; 10 | public DateTime CreationDateTime; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Models/TreeNodeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit.Internal.Models 2 | { 3 | internal struct TreeNodeInfo 4 | { 5 | public byte[] DirectoryData { get; set; } 6 | public uint Offset { get; set; } 7 | public string Path { get; set; } 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Models/CCIDetail.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace XboxToolkit.Internal.Models 4 | { 5 | internal struct CCIDetail 6 | { 7 | public Stream Stream; 8 | public CCIIndex[] IndexInfo; 9 | public long StartSector; 10 | public long EndSector; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /XboxToolkit/Models/Genre.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit.Models 2 | { 3 | public struct Genre 4 | { 5 | public uint Id { get; set; } 6 | public string Name { get; set; } 7 | 8 | public Genre(uint id, string name) 9 | { 10 | Id = id; 11 | Name = name; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /XboxToolkit/ProcessingOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XboxToolkit 4 | { 5 | [Flags] 6 | public enum ProcessingOptions 7 | { 8 | OneToOneCopy = 0, 9 | RemoveVideoPartition = 1, 10 | ScrubSectors = 2, 11 | TrimSectors = 4, 12 | All = RemoveVideoPartition | ScrubSectors | TrimSectors 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Models/GodDetails.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit.Internal.Models 2 | { 3 | internal struct GODDetails 4 | { 5 | public string DataPath; 6 | public uint DataFileCount; 7 | public uint BaseAddress; 8 | public uint StartingBlock; 9 | public uint SectorCount; 10 | public bool IsEnhancedGDF; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/ContainerBuilder/FileEntry.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit.Internal.ContainerBuilder 2 | { 3 | internal class FileEntry 4 | { 5 | public string RelativePath { get; set; } = string.Empty; 6 | public string FullPath { get; set; } = string.Empty; 7 | public uint Size { get; set; } 8 | public uint Sector { get; set; } 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /XboxToolkit/Models/Xbe/XbeSummary.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit.Models.Xbe 2 | { 3 | public class XbeSummary 4 | { 5 | public string? TitleId; 6 | public string? TitleName; 7 | public string? Version; 8 | public string? GameRegion; 9 | public string? OriginalName; 10 | public string? CleanedName; 11 | public string? AllowedMedia; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /XboxToolkit/Interface/ISectorDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XboxToolkit.Interface 4 | { 5 | public interface ISectorDecoder : IDisposable 6 | { 7 | public bool Init(); 8 | public XgdInfo GetXgdInfo(); 9 | public uint TotalSectors(); 10 | public uint SectorSize(); 11 | public bool TryReadSector(long sector, out byte[] sectorData); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Xbe/XbeTls.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace XboxToolkit.Internal.Xbe 4 | { 5 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 6 | internal struct XbeTls 7 | { 8 | public uint DataStartAddr; 9 | public uint DataEndAddr; 10 | public uint TlsIndexAddr; 11 | public uint TlsCallbackAddr; 12 | public uint SizeofZeroFill; 13 | public uint Characteristics; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/ContainerBuilder/DirectoryEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace XboxToolkit.Internal.ContainerBuilder 4 | { 5 | internal class DirectoryEntry 6 | { 7 | public string Path { get; set; } = string.Empty; 8 | public List Files { get; set; } = new List(); 9 | public List Subdirectories { get; set; } = new List(); 10 | public uint Sector { get; set; } 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /XboxToolkit/Models/Dds/DdsPixelFormat.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace XboxToolkit.Models.Dds 4 | { 5 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 6 | public struct DdsPixelFormat 7 | { 8 | public uint Size; 9 | public uint Flags; 10 | public uint FourCC; 11 | public uint RGBBitCount; 12 | public uint RBitMask; 13 | public uint GBitMask; 14 | public uint BBitMask; 15 | public uint ABitMask; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /XboxToolkit/Models/GenreType.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit.Models 2 | { 3 | public enum GenreType 4 | { 5 | Unknown = 0, 6 | Other = 3001, 7 | ActionAdventure = 3002, 8 | Family = 3005, 9 | Fighting = 3006, 10 | Music = 3007, 11 | Platformer = 3008, 12 | RacingFlying = 3009, 13 | RolePlaying = 3010, 14 | Shooter = 3011, 15 | StrategySimulation = 3012, 16 | SportsRecreation = 3013, 17 | BoardCard = 3018, 18 | Classics = 3019, 19 | PuzzleTrivia = 3022, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Xbe/XbeLibraryVersion.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace XboxToolkit.Internal.Xbe 4 | { 5 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 6 | internal struct XbeLibraryVersion 7 | { 8 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] 9 | public byte[] Name; 10 | 11 | public short MajorVersion; 12 | public short MinorVersion; 13 | public short BuildVersion; 14 | public short Flasgs; // 13 bits QfeVersion, 2 bits Approved (0-no, 1-possibly, 2-yes), 1 bit Debug Build (0-no, 1-yes) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /XboxToolkit/Interface/IContainerReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace XboxToolkit.Interface 5 | { 6 | public interface IContainerReader 7 | { 8 | public SectorDecoder GetDecoder(); 9 | public bool TryMount(); 10 | public void Dismount(); 11 | public int GetMountCount(); 12 | public bool TryExtractFiles(string destFilePath, Action? progress = null); 13 | public bool TryGetDataSectors(out HashSet dataSectors); 14 | public bool TryGetDefault(out byte[] defaultData, out ContainerType containerType); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/StructUtility.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace XboxToolkit.Internal 5 | { 6 | internal static class StructUtility 7 | { 8 | public static T? ByteToType(BinaryReader reader) 9 | { 10 | byte[] bytes = reader.ReadBytes(Marshal.SizeOf(typeof(T))); 11 | GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); 12 | T? theStructure = (T?)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); 13 | handle.Free(); 14 | return theStructure; 15 | } 16 | 17 | public static int SizeOf() 18 | { 19 | return Marshal.SizeOf(typeof(T)); 20 | } 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Models/XgdHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace XboxToolkit.Internal.Models 5 | { 6 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 7 | internal class XgdHeader 8 | { 9 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] 10 | public byte[] Magic = Array.Empty(); 11 | 12 | public uint RootDirSector; 13 | 14 | public uint RootDirSize; 15 | 16 | public long CreationFileTime; 17 | 18 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x7c8)] 19 | public byte[] Padding = Array.Empty(); 20 | 21 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] 22 | public byte[] MagicTail = Array.Empty(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /XboxToolkit/Models/Dds/DdsHeader.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace XboxToolkit.Models.Dds 4 | { 5 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 6 | public struct DdsHeader 7 | { 8 | public uint Size; 9 | public uint Flags; 10 | public uint Height; 11 | public uint Width; 12 | public uint PitchOrLinearSize; 13 | public uint Depth; 14 | public uint MipMapCount; 15 | 16 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)] 17 | public uint[] Reserved1; 18 | 19 | public DdsPixelFormat PixelFormat; 20 | 21 | public uint Caps; 22 | public uint Caps2; 23 | public uint Caps3; 24 | public uint Caps4; 25 | public uint Reserved2; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /XboxToolkitTest/XboxToolkitTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Always 18 | 19 | 20 | Always 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Xbe/XbeSectionHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace XboxToolkit.Internal.Xbe 5 | { 6 | [Flags] 7 | internal enum XbeSectionFlags : uint 8 | { 9 | Writable = 1, 10 | Preload = 2, 11 | Executable = 4, 12 | Inserted_File = 8, 13 | Head_Page_Read_Only = 16, 14 | Tail_Page_Read_Only = 32 15 | } 16 | 17 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 18 | internal struct XbeSectionHeader 19 | { 20 | public XbeSectionFlags Flags; 21 | public uint Virtual_Addr; 22 | public uint Virtual_Size; 23 | public uint Raw_Addr; 24 | public uint Sizeof_Raw; 25 | public uint Section_Name_Addr; 26 | public uint Section_Reference_Count; 27 | public uint Head_Shared_Ref_Count_Addr; 28 | public uint Tail_Shared_Ref_Count_Addr; 29 | 30 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] 31 | public byte[] Section_Digest; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /XboxToolkit/Models/MarketplaceMetaData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XboxToolkit.Models 4 | { 5 | public struct MarketplaceMetaData 6 | { 7 | public string TitleId; 8 | public string Title; 9 | public string Developer; 10 | public string Publisher; 11 | public GenreType Genre; 12 | public string Description; 13 | public byte[] TitleImage; 14 | public byte[] BackgroundImage; 15 | public byte[] BannerImage; 16 | public byte[] BoxArtImage; 17 | 18 | public MarketplaceMetaData() 19 | { 20 | TitleId = string.Empty; 21 | Description = string.Empty; 22 | Developer = string.Empty; 23 | Genre = GenreType.Unknown; 24 | Publisher = string.Empty; 25 | Title = string.Empty; 26 | TitleImage = Array.Empty(); 27 | BackgroundImage = Array.Empty(); 28 | BannerImage = Array.Empty(); 29 | BoxArtImage = Array.Empty(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /XboxToolkit/Models/XexMetaData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XboxToolkit.Models 4 | { 5 | public class XexMetaData 6 | { 7 | public XexRegion GameRegion; 8 | 9 | public uint TitleId; 10 | 11 | public uint MediaId; 12 | 13 | public uint Version; 14 | 15 | public uint BaseVersion; 16 | 17 | public uint DiscNum; 18 | 19 | public uint DiscTotal; 20 | 21 | public string TitleName; 22 | 23 | public string Description; 24 | 25 | public string Publisher; 26 | 27 | public string Developer; 28 | 29 | public string Genre; 30 | 31 | public byte[] Thumbnail; 32 | 33 | public string Checksum; 34 | 35 | public XexMetaData() 36 | { 37 | TitleName = string.Empty; 38 | Description = string.Empty; 39 | Publisher = string.Empty; 40 | Developer = string.Empty; 41 | Genre = string.Empty; 42 | Thumbnail = Array.Empty(); 43 | Checksum = string.Empty; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /XboxToolkit/UnicodeHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace XboxToolkit 4 | { 5 | public class UnicodeHelper 6 | { 7 | public static string GetUtf8String(byte[] buffer) 8 | { 9 | var length = 0; 10 | for (var i = 0; i < buffer.Length; i++) 11 | { 12 | if (buffer[i] != 0) 13 | { 14 | length++; 15 | continue; 16 | } 17 | break; 18 | } 19 | return length == 0 ? string.Empty : Encoding.UTF8.GetString(buffer, 0, length); 20 | } 21 | 22 | public static string GetUnicodeString(byte[] buffer) 23 | { 24 | var length = 0; 25 | for (var i = 0; i < buffer.Length; i += 2) 26 | { 27 | if (buffer[i] != 0 || buffer[i + 1] != 0) 28 | { 29 | length += 2; 30 | continue; 31 | } 32 | break; 33 | } 34 | return length == 0 ? string.Empty : Encoding.Unicode.GetString(buffer, 0, length); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit.Internal 2 | { 3 | internal static class Constants 4 | { 5 | public const string XEX_FILE_NAME = "default.xex"; 6 | public const string XBE_FILE_NAME = "default.xbe"; 7 | public const string XGD_IMAGE_MAGIC = "MICROSOFT*XBOX*MEDIA"; 8 | public const uint XGD_SECTOR_SIZE = 0x800; 9 | public const uint XGD_ISO_BASE_SECTOR = 0x20; 10 | public const uint XGD_MAGIC_SECTOR_XDKI = XGD_ISO_BASE_SECTOR; 11 | 12 | public const uint XGD_MAGIC_SECTOR_XGD1 = 0x30620; 13 | 14 | public const uint XGD_MAGIC_SECTOR_XGD2 = 0x1FB40; 15 | public const uint XGD2_PFI_OFFSET = 0xFD8E800; 16 | public const uint XGD2_DMI_OFFSET = 0xFD8F000; 17 | public const uint XGD2_SS_OFFSET = 0xFD8F800; 18 | 19 | public const uint XGD_MAGIC_SECTOR_XGD3 = 0x4120; 20 | public const uint XGD3_PFI_OFFSET = 0x2076800; 21 | public const uint XGD3_DMI_OFFSET = 0x2077000; 22 | public const uint XGD3_SS_OFFSET = 0x2077800; 23 | 24 | public const uint SVOD_START_SECTOR = XGD_ISO_BASE_SECTOR; 25 | 26 | public const uint NXE_CONTAINER_TYPE = 0x4000; 27 | public const uint GOD_CONTAINER_TYPE = 0x7000; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Xbe/XbeHheader.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace XboxToolkit.Internal.Xbe 4 | { 5 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 6 | internal struct XbeHheader 7 | { 8 | public uint Magic; 9 | 10 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)] 11 | public byte[] Dig_Sig; 12 | 13 | public uint Base; 14 | public uint Sizeof_Headers; 15 | public uint Sizeof_Image; 16 | public uint Sizeof_Image_Header; 17 | public uint Time_Date; 18 | public uint Certificate_Addr; 19 | public uint Sections; 20 | public uint Section_Headers_Addr; 21 | public uint Init_Flags; // 1 bit Mount util drive, 1 bit Format util drive, 1 bit Limit dev kit to 64mb, 1 bit Dont setup hdd, 28 bits unused 22 | public uint Entry; 23 | public uint Tls_Addr; 24 | public uint Pe_Stack_Commit; 25 | public uint Pe_Heap_Reserve; 26 | public uint Pe_Heap_Commit; 27 | public uint Pe_Base_Addr; 28 | public uint Pe_Sizeof_Image; 29 | public uint Pe_Checksum; 30 | public uint Pe_Time_Date; 31 | public uint Debug_Pathname_Addr; 32 | public uint Debug_Filename_Addr; 33 | public uint Debug_Unicode_Filename_Addr; 34 | public uint Kernel_image_Thunk_Addr; 35 | public uint Nonkernel_Import_Dir_Addr; 36 | public uint Library_Versions; 37 | public uint Library_Versions_Addr; 38 | public uint Kernel_Library_Version_Addr; 39 | public uint Xapi_Library_Version_Addr; 40 | public uint Logo_Bitmap_Addr; 41 | public uint Logo_Bitmap_Size; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /XboxToolkit.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.6.33815.320 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XboxToolkitTest", "XboxToolkitTest\XboxToolkitTest.csproj", "{0984BE64-EA5E-457F-A46A-07EDB68CCFB2}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XboxToolkit", "XboxToolkit\XboxToolkit.csproj", "{E03B57B7-E90C-4FB7-91A0-EAC5ABA3E799}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {0984BE64-EA5E-457F-A46A-07EDB68CCFB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {0984BE64-EA5E-457F-A46A-07EDB68CCFB2}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {0984BE64-EA5E-457F-A46A-07EDB68CCFB2}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {0984BE64-EA5E-457F-A46A-07EDB68CCFB2}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {E03B57B7-E90C-4FB7-91A0-EAC5ABA3E799}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {E03B57B7-E90C-4FB7-91A0-EAC5ABA3E799}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {E03B57B7-E90C-4FB7-91A0-EAC5ABA3E799}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {E03B57B7-E90C-4FB7-91A0-EAC5ABA3E799}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {1CC297C4-8C80-40F6-BC5D-4B88FE9A5075} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Decoders/ISOSectorDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using XboxToolkit.Interface; 4 | using XboxToolkit.Internal.Models; 5 | 6 | namespace XboxToolkit.Internal.Decoders 7 | { 8 | internal class ISOSectorDecoder : SectorDecoder 9 | { 10 | private readonly ISODetail[] mISODetails; 11 | private readonly object mMutex; 12 | private bool mDisposed; 13 | 14 | public ISOSectorDecoder(ISODetail[] isoDetails) 15 | { 16 | mISODetails = isoDetails; 17 | mMutex = new object(); 18 | mDisposed = false; 19 | } 20 | 21 | public override uint TotalSectors() 22 | { 23 | return (uint)(mISODetails.Max(s => s.EndSector) + 1); 24 | } 25 | 26 | public override bool TryReadSector(long sector, out byte[] sectorData) 27 | { 28 | sectorData = new byte[Constants.XGD_SECTOR_SIZE]; 29 | lock (mMutex) 30 | { 31 | foreach (var isoDetail in mISODetails) 32 | { 33 | if (sector >= isoDetail.StartSector && sector <= isoDetail.EndSector) 34 | { 35 | isoDetail.Stream.Position = (sector - isoDetail.StartSector) * Constants.XGD_SECTOR_SIZE; 36 | var bytesRead = isoDetail.Stream.Read(sectorData, 0, (int)Constants.XGD_SECTOR_SIZE); 37 | return bytesRead == Constants.XGD_SECTOR_SIZE; 38 | } 39 | } 40 | return false; 41 | } 42 | } 43 | 44 | public override void Dispose() 45 | { 46 | Dispose(true); 47 | GC.SuppressFinalize(this); 48 | } 49 | 50 | protected virtual void Dispose(bool disposing) 51 | { 52 | if (mDisposed == false) 53 | { 54 | if (disposing) 55 | { 56 | foreach (var isoDetail in mISODetails) 57 | { 58 | isoDetail.Stream.Dispose(); 59 | } 60 | } 61 | mDisposed = true; 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using XboxToolkit.Internal.Models; 5 | 6 | namespace XboxToolkit.Internal 7 | { 8 | internal class Helpers 9 | { 10 | public static T? ByteToType(BinaryReader reader) 11 | { 12 | byte[] bytes = reader.ReadBytes(Marshal.SizeOf(typeof(T))); 13 | GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); 14 | T? theStructure = (T?)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); 15 | handle.Free(); 16 | return theStructure; 17 | } 18 | 19 | public static int SizeOf() 20 | { 21 | return Marshal.SizeOf(typeof(T)); 22 | } 23 | 24 | public static uint ConvertEndian(uint value) 25 | { 26 | return 27 | (value & 0x000000ff) << 24 | 28 | (value & 0x0000ff00) << 8 | 29 | (value & 0x00ff0000) >> 8 | 30 | (value & 0xff000000) >> 24; 31 | } 32 | 33 | public static ushort ConvertEndian(ushort value) 34 | { 35 | return (ushort)( 36 | (value & 0x000ff) << 8 | 37 | (value & 0xff00) >> 8 38 | ); 39 | } 40 | 41 | public static XgdHeader? GetXgdHeaer(byte[] sector) 42 | { 43 | using var sectorStream = new MemoryStream(sector); 44 | using var sectorReader = new BinaryReader(sectorStream); 45 | var header = ByteToType(sectorReader); 46 | return header; 47 | } 48 | 49 | public static uint RoundToMultiple(uint size, uint multiple) 50 | { 51 | return ((size + multiple - 1) / multiple) * multiple; 52 | } 53 | 54 | public static bool IsEqualTo(float a, float b) 55 | { 56 | return Math.Abs(a - b) < 0.001; 57 | } 58 | 59 | public static void FillArray(T[] array, T value) 60 | { 61 | for (var i = 0; i < array.Length; i++) 62 | { 63 | array[i] = value; 64 | } 65 | } 66 | 67 | public static void FillArray(T[] array, T value, int startIndex, int count) 68 | { 69 | for (var i = startIndex; i < startIndex + count && i < array.Length; i++) 70 | { 71 | array[i] = value; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /XboxToolkit/XboxToolkit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | true 6 | 11.0 7 | enable 8 | 9 | 10 | 11 | A comprehensive .NET library for working with Xbox 360 and Xbox Original game containers, executables, and metadata. Supports reading ISO, CCI, and GOD containers, extracting XEX/XBE metadata, decrypting/decompressing XEX files, creating ISO files from folders, and fetching marketplace metadata. Includes image processing: XPR/DDS texture conversion, XBE image extraction/replacement, and BIOS logo encoding/decoding. Requires .NET 6.0+. Includes cross-platform native libraries for Windows, Linux, and macOS. 12 | 1.3.1 13 | EqUiNoX 14 | true 15 | XboxToolkit 16 | XboxToolkit 17 | 18 | $(AssemblyVersion)$(PackagePrereleaseIdentifier) 19 | Xbox;Xbox360;Xbox-Original;ISO;CCI;GOD;XEX;XBE;XPR;DDS;Game-Container;Xbox-Toolkit;Xbox360-Toolkit;Container-Reader;Xex-Extractor;Texture-Converter;Image-Processing;BIOS-Logo 20 | https://github.com/Team-Resurgent/XboxToolkit 21 | XboxToolkit 22 | True 23 | XboxToolkit 24 | Team Resurgent 25 | Readme.md 26 | 1bc5a42d-53e1-4b27-9f03-fec2cceadbd5 27 | XboxToolkit 28 | https://github.com/Team-Resurgent/XboxToolkit 29 | 1.3.1 30 | GPL-3.0-only 31 | 32 | 33 | 34 | 35 | runtimes/win-x64/native/libXexUnpack.dll 36 | true 37 | 38 | 39 | runtimes/linux-x64/native/libXexUnpack.so 40 | true 41 | 42 | 43 | runtimes/osx/native/libXexUnpack.dylib 44 | true 45 | 46 | 47 | 48 | 49 | 50 | True 51 | \ 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/XexUnpack.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace XboxToolkit.Internal 6 | { 7 | internal unsafe static class XexUnpack 8 | { 9 | [DllImport("libXexUnpack", CallingConvention = CallingConvention.Cdecl)] 10 | private static extern void LZXUnpack(byte* inputData, uint inputSize, byte* outputData, uint outputDataSize, uint windowSize, ref uint error); 11 | 12 | private static bool OptimizePackData(byte[] input, uint firstDataSize, out byte[] packedData) 13 | { 14 | var buffer = new byte[input.Length]; 15 | 16 | uint currLen = firstDataSize; 17 | uint nextLen = 1; 18 | 19 | uint compressedSz = 0; 20 | uint currPos = 0; 21 | uint lastPos = 0; 22 | 23 | using var ms = new MemoryStream(input); 24 | using var br = new BinaryReader(ms); 25 | 26 | ms.Position = currPos; 27 | 28 | while ((currPos < input.Length) && (nextLen != 0)) 29 | { 30 | ms.Position = currPos; 31 | nextLen = Helpers.ConvertEndian(br.ReadUInt32()); 32 | currPos += 0x18; 33 | 34 | uint innerLen = 1; 35 | do 36 | { 37 | ms.Position = currPos; 38 | innerLen = Helpers.ConvertEndian(br.ReadUInt16()); 39 | currPos += 2; 40 | 41 | if (innerLen > 0) 42 | { 43 | if (compressedSz + innerLen >= buffer.Length || currPos + innerLen >= input.Length) 44 | { 45 | packedData = Array.Empty(); 46 | return false; 47 | } 48 | Array.Copy(input, currPos, buffer, compressedSz, innerLen); 49 | currPos += innerLen; 50 | compressedSz += innerLen; 51 | } 52 | 53 | } while (innerLen > 0); 54 | 55 | currPos = lastPos + currLen; 56 | currLen = nextLen; 57 | lastPos = currPos; 58 | } 59 | 60 | packedData = buffer.AsSpan(0, (int)compressedSz).ToArray(); 61 | return true; 62 | } 63 | 64 | public unsafe static bool UnpackXexData(byte[] input, uint imageSize, uint windowSize, uint firstSize, out byte[] output) 65 | { 66 | if (OptimizePackData(input, firstSize, out var optimizedData) == false) 67 | { 68 | output = Array.Empty(); 69 | return false; 70 | } 71 | return LZXUnpack(optimizedData, imageSize, windowSize, out output); 72 | } 73 | 74 | public unsafe static bool LZXUnpack(byte[] input, uint imageSize, uint windowSize, out byte[] output) 75 | { 76 | output = new byte[imageSize]; 77 | fixed (byte* outputArray = output) 78 | { 79 | fixed (byte* inputArray = input) 80 | { 81 | uint error = 0xffffff; 82 | LZXUnpack(inputArray, (uint)input.Length, outputArray, (uint)output.Length, windowSize, ref error); 83 | return error == 0; 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Decoders/GodSectorDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using XboxToolkit.Interface; 4 | using XboxToolkit.Internal.Models; 5 | 6 | namespace XboxToolkit.Internal.Decoders 7 | { 8 | internal class GODSectorDecoder : SectorDecoder 9 | { 10 | private GODDetails mGODDetails; 11 | private FileStream[] mFileStreams; 12 | private object mMutex; 13 | private bool mDisposed; 14 | 15 | public GODSectorDecoder(GODDetails godDetails) 16 | { 17 | mGODDetails = godDetails; 18 | mFileStreams = new FileStream[mGODDetails.DataFileCount]; 19 | for (var i = 0; i < mGODDetails.DataFileCount; i++) 20 | { 21 | var filePath = Path.Combine(mGODDetails.DataPath, string.Format("Data{0:D4}", i)); 22 | mFileStreams[i] = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); 23 | } 24 | mMutex = new object(); 25 | mDisposed = false; 26 | } 27 | 28 | private long SectorToAddress(long sector, out uint dataFileIndex) 29 | { 30 | if (sector == Constants.SVOD_START_SECTOR || sector == Constants.SVOD_START_SECTOR + 1) 31 | { 32 | dataFileIndex = 0; 33 | return mGODDetails.BaseAddress + (sector - Constants.SVOD_START_SECTOR) * Constants.XGD_SECTOR_SIZE; 34 | } 35 | 36 | var adjustedSector = sector - mGODDetails.StartingBlock * 2 + (mGODDetails.IsEnhancedGDF ? 2 : 0); 37 | dataFileIndex = (uint)(adjustedSector / 0x14388); 38 | if (dataFileIndex > mGODDetails.DataFileCount) 39 | { 40 | dataFileIndex = 0; 41 | } 42 | var dataSector = adjustedSector % 0x14388; 43 | var dataBlock = dataSector / 0x198; 44 | dataSector %= 0x198; 45 | 46 | var dataFileOffset = (dataSector + dataBlock * 0x198) * 0x800; 47 | dataFileOffset += 0x1000; 48 | dataFileOffset += dataBlock * 0x1000 + 0x1000; 49 | return dataFileOffset; 50 | } 51 | 52 | public override uint TotalSectors() 53 | { 54 | return mGODDetails.SectorCount; 55 | } 56 | 57 | public override bool TryReadSector(long sector, out byte[] sectorData) 58 | { 59 | var dataOffset = SectorToAddress(sector, out var dataFileIndex); 60 | if (dataOffset < 0) 61 | { 62 | sectorData = new byte[Constants.XGD_SECTOR_SIZE]; 63 | return true; 64 | } 65 | 66 | sectorData = new byte[Constants.XGD_SECTOR_SIZE]; 67 | lock (mMutex) 68 | { 69 | mFileStreams[dataFileIndex].Position = dataOffset; 70 | var bytesRead = mFileStreams[dataFileIndex].Read(sectorData, 0, (int)Constants.XGD_SECTOR_SIZE); 71 | return bytesRead == Constants.XGD_SECTOR_SIZE; 72 | } 73 | } 74 | 75 | public override void Dispose() 76 | { 77 | Dispose(true); 78 | GC.SuppressFinalize(this); 79 | } 80 | 81 | protected virtual void Dispose(bool disposing) 82 | { 83 | if (mDisposed == false) 84 | { 85 | if (disposing) 86 | { 87 | for (var i = 0; i < mGODDetails.DataFileCount; i++) 88 | { 89 | mFileStreams[i].Dispose(); 90 | } 91 | } 92 | mDisposed = true; 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/ContainerBuilder/SectorAllocator.cs: -------------------------------------------------------------------------------- 1 | namespace XboxToolkit.Internal.ContainerBuilder 2 | { 3 | internal class SectorAllocator 4 | { 5 | private uint mCurrentSector; 6 | private readonly uint mMagicSector; 7 | private readonly uint mBaseSector; 8 | private uint mDirectoryStartSector; 9 | private bool mDirectoriesAllocated; 10 | 11 | public SectorAllocator(uint magicSector, uint baseSector) 12 | { 13 | mMagicSector = magicSector; 14 | mBaseSector = baseSector; 15 | mCurrentSector = baseSector; 16 | mDirectoriesAllocated = false; 17 | } 18 | 19 | public uint AllocateSectors(uint count) 20 | { 21 | // Skip magic sector if we would write over it 22 | if (mCurrentSector <= mMagicSector && mCurrentSector + count > mMagicSector) 23 | { 24 | mCurrentSector = mMagicSector + 1; 25 | } 26 | 27 | var startSector = mCurrentSector; 28 | mCurrentSector += count; 29 | return startSector; 30 | } 31 | 32 | public uint AllocateDirectorySectors(uint count) 33 | { 34 | // Directories should be allocated after magic sector 35 | // Store where directories start for later file allocation 36 | if (!mDirectoriesAllocated) 37 | { 38 | mDirectoryStartSector = mMagicSector + 1; 39 | mCurrentSector = mDirectoryStartSector; 40 | mDirectoriesAllocated = true; 41 | } 42 | 43 | var startSector = mCurrentSector; 44 | mCurrentSector += count; 45 | return startSector; 46 | } 47 | 48 | public uint AllocateFileSectors(uint count) 49 | { 50 | // Try to allocate files in the gap between base sector and magic sector first 51 | // If that space is full, allocate after magic sector (and after directories if they exist) 52 | if (mCurrentSector < mMagicSector) 53 | { 54 | // Check if we can fit in the gap before magic sector 55 | if (mCurrentSector + count <= mMagicSector) 56 | { 57 | var startSector = mCurrentSector; 58 | mCurrentSector += count; 59 | return startSector; 60 | } 61 | else 62 | { 63 | // Gap is too small, skip to after magic sector 64 | mCurrentSector = mMagicSector + 1; 65 | } 66 | } 67 | 68 | // If we're at or past magic sector, allocate after directories (if they exist) or right after magic sector 69 | if (mDirectoriesAllocated && mCurrentSector < mDirectoryStartSector) 70 | { 71 | // Fill space between magic sector and directories 72 | var startSector = mCurrentSector; 73 | mCurrentSector += count; 74 | return startSector; 75 | } 76 | 77 | // If directories are allocated, make sure we're after them 78 | if (mDirectoriesAllocated && mCurrentSector < mDirectoryStartSector) 79 | { 80 | mCurrentSector = mDirectoryStartSector; 81 | } 82 | 83 | // Allocate after magic sector (and after directories if they exist) 84 | var fileStartSector = mCurrentSector; 85 | mCurrentSector += count; 86 | return fileStartSector; 87 | } 88 | 89 | public uint GetTotalSectors() 90 | { 91 | return mCurrentSector; 92 | } 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Decoders/CCISectorDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using XboxToolkit.Interface; 4 | using XboxToolkit.Internal.Models; 5 | 6 | namespace XboxToolkit.Internal.Decoders 7 | { 8 | internal class CCISectorDecoder : SectorDecoder 9 | { 10 | private readonly CCIDetail[] mCCIDetails; 11 | private readonly object mMutex; 12 | private bool mDisposed; 13 | 14 | public CCISectorDecoder(CCIDetail[] cciDetails) 15 | { 16 | mCCIDetails = cciDetails; 17 | mMutex = new object(); 18 | mDisposed = false; 19 | } 20 | 21 | public override uint TotalSectors() 22 | { 23 | return (uint)(mCCIDetails.Max(s => s.EndSector) + 1); 24 | } 25 | 26 | public override bool TryReadSector(long sector, out byte[] sectorData) 27 | { 28 | sectorData = new byte[Constants.XGD_SECTOR_SIZE]; 29 | lock (mMutex) 30 | { 31 | foreach (var cciDetail in mCCIDetails) 32 | { 33 | if (sector >= cciDetail.StartSector && sector <= cciDetail.EndSector) 34 | { 35 | var sectorOffset = sector - cciDetail.StartSector; 36 | var position = cciDetail.IndexInfo[sectorOffset].Value; 37 | var LZ4Compressed = cciDetail.IndexInfo[sectorOffset].LZ4Compressed; 38 | var size = (int)(cciDetail.IndexInfo[sectorOffset + 1].Value - position); 39 | 40 | cciDetail.Stream.Position = (long)position; 41 | if (size != Constants.XGD_SECTOR_SIZE || LZ4Compressed) 42 | { 43 | var padding = cciDetail.Stream.ReadByte(); 44 | var decompressBuffer = new byte[size]; 45 | var decompressBytesRead = cciDetail.Stream.Read(decompressBuffer, 0, size); 46 | if (decompressBytesRead != size) 47 | { 48 | sectorData = Array.Empty(); 49 | return false; 50 | } 51 | var decodeBuffer = new byte[Constants.XGD_SECTOR_SIZE]; 52 | var decompressedSize = K4os.Compression.LZ4.LZ4Codec.Decode(decompressBuffer, 0, size - (padding + 1), decodeBuffer, 0, (int)Constants.XGD_SECTOR_SIZE); 53 | if (decompressedSize < 0) 54 | { 55 | sectorData = Array.Empty(); 56 | return false; 57 | } 58 | sectorData = decodeBuffer; 59 | return true; 60 | } 61 | 62 | sectorData = new byte[Constants.XGD_SECTOR_SIZE]; 63 | var sectorBytesRead = cciDetail.Stream.Read(sectorData, 0, (int)Constants.XGD_SECTOR_SIZE); 64 | return sectorBytesRead == Constants.XGD_SECTOR_SIZE; 65 | } 66 | } 67 | return false; 68 | } 69 | } 70 | 71 | public override void Dispose() 72 | { 73 | Dispose(true); 74 | GC.SuppressFinalize(this); 75 | } 76 | 77 | protected virtual void Dispose(bool disposing) 78 | { 79 | if (mDisposed == false) 80 | { 81 | if (disposing) 82 | { 83 | foreach (var cciDetail in mCCIDetails) 84 | { 85 | cciDetail.Stream.Dispose(); 86 | } 87 | } 88 | mDisposed = true; 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /XboxToolkit/ISOContainerReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using XboxToolkit.Interface; 5 | using XboxToolkit.Internal; 6 | using XboxToolkit.Internal.Decoders; 7 | using XboxToolkit.Internal.Models; 8 | 9 | namespace XboxToolkit 10 | { 11 | public class ISOContainerReader : ContainerReader, IDisposable 12 | { 13 | private string mFilePath; 14 | private int mMountCount; 15 | private SectorDecoder? mSectorDecoder; 16 | private bool mDisposed; 17 | 18 | public ISOContainerReader(string filePath) 19 | { 20 | mFilePath = filePath; 21 | mMountCount = 0; 22 | } 23 | 24 | public override SectorDecoder GetDecoder() 25 | { 26 | if (mSectorDecoder == null) 27 | { 28 | throw new Exception("Container not mounted."); 29 | } 30 | return mSectorDecoder; 31 | } 32 | 33 | public static bool IsISO(string filePath) 34 | { 35 | if (File.Exists(filePath) == false) 36 | { 37 | return false; 38 | } 39 | return Path.GetExtension(filePath).Equals(".iso", StringComparison.CurrentCultureIgnoreCase); 40 | } 41 | 42 | public override bool TryMount() 43 | { 44 | try 45 | { 46 | if (mMountCount > 0) 47 | { 48 | mMountCount++; 49 | return true; 50 | } 51 | 52 | if (IsISO(mFilePath) == false) 53 | { 54 | return false; 55 | } 56 | 57 | var fileSlices = ContainerUtility.GetSlicesFromFile(mFilePath); 58 | 59 | var isoDetails = new List(); 60 | 61 | var sectorCount = 0L; 62 | foreach (var fileSlice in fileSlices) 63 | { 64 | var stream = new FileStream(fileSlice, FileMode.Open, FileAccess.Read, FileShare.Read); 65 | var sectors = stream.Length / Constants.XGD_SECTOR_SIZE; 66 | var isoDetail = new ISODetail() 67 | { 68 | Stream = stream, 69 | StartSector = sectorCount, 70 | EndSector = sectorCount + sectors - 1 71 | }; 72 | isoDetails.Add(isoDetail); 73 | sectorCount += sectors; 74 | } 75 | 76 | mSectorDecoder = new ISOSectorDecoder(isoDetails.ToArray()); 77 | if (mSectorDecoder.Init() == false) 78 | { 79 | return false; 80 | } 81 | 82 | mMountCount++; 83 | return true; 84 | } 85 | catch (Exception ex) 86 | { 87 | System.Diagnostics.Debug.Print(ex.ToString()); 88 | return false; 89 | } 90 | } 91 | 92 | public override void Dismount() 93 | { 94 | if (mMountCount == 0) 95 | { 96 | return; 97 | } 98 | mMountCount--; 99 | } 100 | 101 | public override int GetMountCount() 102 | { 103 | return mMountCount; 104 | } 105 | 106 | public override void Dispose() 107 | { 108 | Dispose(true); 109 | GC.SuppressFinalize(this); 110 | } 111 | 112 | protected virtual void Dispose(bool disposing) 113 | { 114 | if (mDisposed == false) 115 | { 116 | if (disposing) 117 | { 118 | mSectorDecoder?.Dispose(); 119 | } 120 | mDisposed = true; 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /XboxToolkit/BiosLogoUtility.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp; 2 | using SixLabors.ImageSharp.PixelFormats; 3 | using SixLabors.ImageSharp.Processing; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | 8 | namespace XboxToolkit 9 | { 10 | public class BiosLogoUtility 11 | { 12 | private void DecodeLogo(byte[] imageData, ref int index, ref int size, ref int intensity) 13 | { 14 | int value1 = imageData[index]; 15 | bool type1 = (value1 & 0x01) > 0; 16 | if (type1) 17 | { 18 | size = (value1 & 0x0e) >> 1; 19 | intensity = value1 & 0xf0; 20 | index += 1; 21 | return; 22 | } 23 | 24 | int value2 = imageData[index + 1] << 8 | imageData[index]; 25 | bool type2 = (value2 & 0x0002) > 0; 26 | if (type2) 27 | { 28 | size = (value2 & 0x0ffc) >> 2; 29 | intensity = (value2 & 0xf000) >> 8; 30 | index += 2; 31 | return; 32 | } 33 | 34 | throw new ArgumentOutOfRangeException(); 35 | } 36 | 37 | public void DecodeLogoImage(byte[]? input, out byte[]? output) 38 | { 39 | output = null; 40 | 41 | if (input == null) 42 | { 43 | return; 44 | } 45 | 46 | using var image = new Image(100, 17); 47 | 48 | int imageOffset = 0; 49 | int index = 0; 50 | int size = 0; 51 | int intensity = 0; 52 | int x = 0; 53 | int y = 0; 54 | 55 | while (index < input.Length) 56 | { 57 | DecodeLogo(input, ref index, ref size, ref intensity); 58 | for (var i = 0; i < size; i++) 59 | { 60 | image[x, y] = new L8((byte)intensity); 61 | imageOffset++; 62 | 63 | x++; 64 | if (x == image.Width) 65 | { 66 | x = 0; 67 | y++; 68 | } 69 | } 70 | } 71 | using var imageStream = new MemoryStream(); 72 | image.SaveAsPng(imageStream); 73 | output = imageStream.ToArray(); 74 | } 75 | 76 | private void EncodeLogo(int size, int intensity, List imageData) 77 | { 78 | while (size > 0) 79 | { 80 | if (size <= 7) 81 | { 82 | byte value1 = 1; 83 | value1 |= (byte)(size << 1); 84 | value1 |= (byte)(intensity << 4); 85 | imageData.Add(value1); 86 | size = 0; 87 | break; 88 | } 89 | int clampedSize = Math.Min(size, 1023); 90 | int value2 = 2; 91 | value2 |= clampedSize << 2; 92 | value2 |= intensity << 12; 93 | imageData.Add((byte)(value2 & 0xff)); 94 | imageData.Add((byte)(value2 >> 8 & 0xff)); 95 | size -= clampedSize; 96 | } 97 | } 98 | 99 | public void EncodeLogoImage(byte[]? input, int wodth, int height, out byte[]? output) 100 | { 101 | output = null; 102 | 103 | if (input == null) 104 | { 105 | return; 106 | } 107 | 108 | var imageData = new List(); 109 | 110 | using var image = Image.Load(input); 111 | image.Mutate(i => i.Resize(wodth, height)); 112 | 113 | int size = 1; 114 | int intensity = image[0, 0].PackedValue >> 4; 115 | for (int y = 0; y < height; y++) 116 | { 117 | for (int x = 0; x < wodth; x++) 118 | { 119 | int pixelIntensity = image[x, y].PackedValue >> 4; 120 | if (pixelIntensity == intensity) 121 | { 122 | size++; 123 | continue; 124 | } 125 | EncodeLogo(size, intensity, imageData); 126 | intensity = pixelIntensity; 127 | size = 1; 128 | } 129 | } 130 | 131 | output = imageData.ToArray(); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /XboxToolkit/Interface/SectorDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using XboxToolkit.Internal; 3 | using XboxToolkit.Internal.Models; 4 | 5 | namespace XboxToolkit.Interface 6 | { 7 | public abstract class SectorDecoder : ISectorDecoder 8 | { 9 | public bool Init() 10 | { 11 | var found = false; 12 | var baseSector = 0U; 13 | 14 | var header = new XgdHeader(); 15 | 16 | if (TotalSectors() >= Constants.XGD_MAGIC_SECTOR_XDKI) 17 | { 18 | if (TryReadSector(Constants.XGD_MAGIC_SECTOR_XDKI, out var sector) == true) 19 | { 20 | header = Helpers.GetXgdHeaer(sector); 21 | if (header != null && UnicodeHelper.GetUtf8String(header.Magic).Equals(Constants.XGD_IMAGE_MAGIC) && UnicodeHelper.GetUtf8String(header.MagicTail).Equals(Constants.XGD_IMAGE_MAGIC)) 22 | { 23 | baseSector = Constants.XGD_MAGIC_SECTOR_XDKI - Constants.XGD_ISO_BASE_SECTOR; 24 | found = true; 25 | } 26 | } 27 | } 28 | 29 | if (found == false && TotalSectors() >= Constants.XGD_MAGIC_SECTOR_XGD1) 30 | { 31 | if (TryReadSector(Constants.XGD_MAGIC_SECTOR_XGD1, out var sector) == true) 32 | { 33 | header = Helpers.GetXgdHeaer(sector); 34 | if (header != null && UnicodeHelper.GetUtf8String(header.Magic).Equals(Constants.XGD_IMAGE_MAGIC) && UnicodeHelper.GetUtf8String(header.MagicTail).Equals(Constants.XGD_IMAGE_MAGIC)) 35 | { 36 | baseSector = Constants.XGD_MAGIC_SECTOR_XGD1 - Constants.XGD_ISO_BASE_SECTOR; 37 | found = true; 38 | } 39 | } 40 | } 41 | 42 | if (found == false && TotalSectors() >= Constants.XGD_MAGIC_SECTOR_XGD3) 43 | { 44 | if (TryReadSector(Constants.XGD_MAGIC_SECTOR_XGD3, out var sector) == true) 45 | { 46 | header = Helpers.GetXgdHeaer(sector); 47 | if (header != null && UnicodeHelper.GetUtf8String(header.Magic).Equals(Constants.XGD_IMAGE_MAGIC) && UnicodeHelper.GetUtf8String(header.MagicTail).Equals(Constants.XGD_IMAGE_MAGIC)) 48 | { 49 | baseSector = Constants.XGD_MAGIC_SECTOR_XGD3 - Constants.XGD_ISO_BASE_SECTOR; 50 | found = true; 51 | } 52 | } 53 | } 54 | 55 | if (found == false && TotalSectors() >= Constants.XGD_MAGIC_SECTOR_XGD2) 56 | { 57 | if (TryReadSector(Constants.XGD_MAGIC_SECTOR_XGD2, out var sector) == true) 58 | { 59 | header = Helpers.GetXgdHeaer(sector); 60 | if (header != null && UnicodeHelper.GetUtf8String(header.Magic).Equals(Constants.XGD_IMAGE_MAGIC) && UnicodeHelper.GetUtf8String(header.MagicTail).Equals(Constants.XGD_IMAGE_MAGIC)) 61 | { 62 | baseSector = Constants.XGD_MAGIC_SECTOR_XGD2 - Constants.XGD_ISO_BASE_SECTOR; 63 | found = true; 64 | } 65 | } 66 | } 67 | 68 | if (found == true && header != null) 69 | { 70 | mXgdInfo = new XgdInfo 71 | { 72 | BaseSector = baseSector, 73 | RootDirSector = header.RootDirSector, 74 | RootDirSize = header.RootDirSize, 75 | CreationDateTime = DateTime.FromFileTime(header.CreationFileTime) 76 | }; 77 | return true; 78 | } 79 | 80 | return false; 81 | } 82 | 83 | private XgdInfo? mXgdInfo; 84 | public XgdInfo GetXgdInfo() 85 | { 86 | if (mXgdInfo == null) 87 | { 88 | throw new Exception("Sector decoder nor initialized."); 89 | } 90 | return mXgdInfo; 91 | } 92 | 93 | public abstract uint TotalSectors(); 94 | 95 | public uint SectorSize() 96 | { 97 | return Constants.XGD_SECTOR_SIZE; 98 | } 99 | 100 | public abstract bool TryReadSector(long sector, out byte[] sectorData); 101 | 102 | public virtual void Dispose() 103 | { 104 | throw new NotImplementedException(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /XboxToolkit/Models/Xbe/XbeCertificate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using XboxToolkit.Internal; 4 | 5 | namespace XboxToolkit.Models.Xbe 6 | { 7 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 8 | public class XbeCertificate 9 | { 10 | public uint Size; 11 | public uint Time_Date; 12 | public uint Title_Id; 13 | 14 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 80)] 15 | public byte[] Title_Name = Array.Empty(); 16 | 17 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] 18 | public uint[] Alt_Title_Id = Array.Empty(); 19 | 20 | public uint Allowed_Media; 21 | public uint Game_Region; 22 | public uint Game_Ratings; 23 | public uint Disk_Number; 24 | public uint Version; 25 | 26 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] 27 | public byte[] Lan_Key = Array.Empty(); 28 | 29 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] 30 | public byte[] Sig_Key = Array.Empty(); 31 | 32 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)] 33 | public byte[] Title_Alt_Sig_Key = Array.Empty(); 34 | 35 | public static string GameRegionToString(uint region) 36 | { 37 | var gameRegion = string.Empty; 38 | var debug = (region & 0x80000000) == 0x80000000; 39 | if (debug) 40 | { 41 | gameRegion = "DBG"; 42 | } 43 | 44 | var global = (region & 0x00000007) == 0x00000007; 45 | if (global) 46 | { 47 | gameRegion = gameRegion.Length > 0 ? $"GLO-{gameRegion}" : "GLO"; 48 | } 49 | else 50 | { 51 | if ((region & 0x00000004) == 0x00000004) 52 | { 53 | gameRegion = gameRegion.Length > 0 ? $"PAL-{gameRegion}" : "PAL"; 54 | } 55 | if ((region & 0x00000002) == 0x00000002) 56 | { 57 | gameRegion = gameRegion.Length > 0 ? $"JPN-{gameRegion}" : "JPN"; 58 | } 59 | if ((region & 0x00000001) == 0x00000001) 60 | { 61 | gameRegion = gameRegion.Length > 0 ? $"USA-{gameRegion}" : "USA"; 62 | } 63 | } 64 | 65 | return gameRegion.Length == 0 ? "" : gameRegion; 66 | } 67 | 68 | public string AllowedMediaToString(uint media) 69 | { 70 | var allowedMedia = string.Empty; 71 | if ((media & 0x00000001) == 0x00000001) 72 | { 73 | allowedMedia = allowedMedia.Length > 0 ? $"HARD_DISK, {allowedMedia}" : "HARD_DISK"; 74 | } 75 | if ((media & 0x00000002) == 0x00000002) 76 | { 77 | allowedMedia = allowedMedia.Length > 0 ? $"DVD_X2, {allowedMedia}" : "DVD_X2"; 78 | } 79 | if ((media & 0x00000004) == 0x00000004) 80 | { 81 | allowedMedia = allowedMedia.Length > 0 ? $"DVD_CD, {allowedMedia}" : "DVD_CD"; 82 | } 83 | if ((media & 0x00000008) == 0x00000008) 84 | { 85 | allowedMedia = allowedMedia.Length > 0 ? $"CD, {allowedMedia}" : "CD"; 86 | } 87 | if ((media & 0x00000010) == 0x00000010) 88 | { 89 | allowedMedia = allowedMedia.Length > 0 ? $"DVD_5_RO, {allowedMedia}" : "DVD_5_RO"; 90 | } 91 | if ((media & 0x00000020) == 0x00000020) 92 | { 93 | allowedMedia = allowedMedia.Length > 0 ? $"DVD_9_RO, {allowedMedia}" : "DVD_9_RO"; 94 | } 95 | if ((media & 0x00000040) == 0x00000040) 96 | { 97 | allowedMedia = allowedMedia.Length > 0 ? $"DVD_5_RW, {allowedMedia}" : "DVD_5_RW"; 98 | } 99 | if ((media & 0x00000080) == 0x00000080) 100 | { 101 | allowedMedia = allowedMedia.Length > 0 ? $"DVD_9_RW, {allowedMedia}" : "DVD_9_RW"; 102 | } 103 | if ((media & 0x00000100) == 0x00000100) 104 | { 105 | allowedMedia = allowedMedia.Length > 0 ? $"DONGLE, {allowedMedia}" : "DONGLE"; 106 | } 107 | if ((media & 0x00000200) == 0x00000200) 108 | { 109 | allowedMedia = allowedMedia.Length > 0 ? $"MEDIA_BOARD, {allowedMedia}" : "MEDIA_BOARD"; 110 | } 111 | if ((media & 0x40000000) == 0x40000000) 112 | { 113 | allowedMedia = allowedMedia.Length > 0 ? $"NONSECURE_HARD_DISK, {allowedMedia}" : "NONSECURE_HARD_DISK"; 114 | } 115 | if ((media & 0x80000000) == 0x80000000) 116 | { 117 | allowedMedia = allowedMedia.Length > 0 ? $"NONSECURE_MODE, {allowedMedia}" : "NONSECURE_MODE"; 118 | } 119 | return allowedMedia; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /XboxToolkit/GODContainerReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using XboxToolkit.Interface; 4 | using XboxToolkit.Internal; 5 | using XboxToolkit.Internal.Decoders; 6 | using XboxToolkit.Internal.Models; 7 | 8 | namespace XboxToolkit 9 | { 10 | public class GODContainerReader : ContainerReader 11 | { 12 | private string mFilePath; 13 | private int mMountCount; 14 | private SectorDecoder? mSectorDecoder; 15 | private bool mDisposed; 16 | 17 | public GODContainerReader(string filePath) 18 | { 19 | mFilePath = filePath; 20 | mMountCount = 0; 21 | } 22 | 23 | public override SectorDecoder GetDecoder() 24 | { 25 | if (mSectorDecoder == null) 26 | { 27 | throw new Exception("Container not mounted."); 28 | } 29 | return mSectorDecoder; 30 | } 31 | 32 | public static bool IsGOD(string filePath) 33 | { 34 | if (File.Exists(filePath) == false) 35 | { 36 | return false; 37 | } 38 | var dataPath = filePath + ".data"; 39 | return Directory.Exists(dataPath); 40 | } 41 | 42 | public override bool TryMount() 43 | { 44 | try 45 | { 46 | if (mMountCount > 0) 47 | { 48 | mMountCount++; 49 | return true; 50 | } 51 | 52 | if (IsGOD(mFilePath) == false) 53 | { 54 | return false; 55 | } 56 | 57 | using (var fileStream = new FileStream(mFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) 58 | using (var binaryReader = new BinaryReader(fileStream)) 59 | { 60 | var header = Helpers.ByteToType(binaryReader); 61 | 62 | var signatureType = Helpers.ConvertEndian(header.SignatureType); 63 | if (signatureType != (uint)XCONTENT_SIGNATURE_TYPE.LIVE_SIGNED && signatureType != (uint)XCONTENT_SIGNATURE_TYPE.CONSOLE_SIGNED && signatureType != (uint)XCONTENT_SIGNATURE_TYPE.PIRS_SIGNED) 64 | { 65 | return false; 66 | } 67 | 68 | var contentMetaData = Helpers.ByteToType(binaryReader); 69 | var contentType = Helpers.ConvertEndian(contentMetaData.ContentType); 70 | if (contentType != Constants.NXE_CONTAINER_TYPE && contentType != Constants.GOD_CONTAINER_TYPE) 71 | { 72 | return false; 73 | } 74 | 75 | var startingBlock = (uint)(((contentMetaData.SvodVolumeDescriptor.StartingDataBlock2 << 16) & 0xFF0000) | ((contentMetaData.SvodVolumeDescriptor.StartingDataBlock1 << 8) & 0xFF00) | ((contentMetaData.SvodVolumeDescriptor.StartingDataBlock0) & 0xFF)); 76 | var numDataBlocks = (uint)(((contentMetaData.SvodVolumeDescriptor.NumberOfDataBlocks2 << 16) & 0xFF0000) | ((contentMetaData.SvodVolumeDescriptor.NumberOfDataBlocks1 << 8) & 0xFF00) | ((contentMetaData.SvodVolumeDescriptor.NumberOfDataBlocks0) & 0xFF)); 77 | 78 | 79 | var godDetails = new GODDetails(); 80 | godDetails.DataPath = mFilePath + ".data"; 81 | godDetails.DataFileCount = Helpers.ConvertEndian(contentMetaData.DataFiles); 82 | godDetails.IsEnhancedGDF = (contentMetaData.SvodVolumeDescriptor.Features & (1 << 6)) != 0; 83 | godDetails.BaseAddress = godDetails.IsEnhancedGDF ? 0x2000u : 0x12000u; 84 | godDetails.StartingBlock = startingBlock; 85 | godDetails.SectorCount = (numDataBlocks + startingBlock) * 2; 86 | 87 | mSectorDecoder = new GODSectorDecoder(godDetails); 88 | if (mSectorDecoder.Init() == false) 89 | { 90 | return false; 91 | } 92 | 93 | mMountCount++; 94 | return true; 95 | } 96 | } 97 | catch (Exception ex) 98 | { 99 | System.Diagnostics.Debug.Print(ex.ToString()); 100 | return false; 101 | } 102 | } 103 | 104 | public override void Dismount() 105 | { 106 | if (mMountCount == 0) 107 | { 108 | return; 109 | } 110 | mMountCount--; 111 | } 112 | 113 | public override int GetMountCount() 114 | { 115 | return mMountCount; 116 | } 117 | 118 | public override void Dispose() 119 | { 120 | Dispose(true); 121 | GC.SuppressFinalize(this); 122 | } 123 | 124 | protected virtual void Dispose(bool disposing) 125 | { 126 | if (mDisposed == false) 127 | { 128 | if (disposing) 129 | { 130 | mSectorDecoder?.Dispose(); 131 | } 132 | mDisposed = true; 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Models/GodStructs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace XboxToolkit.Internal.Models 4 | { 5 | internal enum XCONTENT_SIGNATURE_TYPE 6 | { 7 | CONSOLE_SIGNED = 0x434F4E20, // CON 8 | LIVE_SIGNED = 0x4C495645, // LIVE 9 | PIRS_SIGNED = 0x50495253 // PIRS 10 | } 11 | 12 | internal enum XCONTENT_VOLUME_TYPE 13 | { 14 | STFS_VOLUME = 0x0, 15 | SVOD_VOLUME = 0x1 16 | } 17 | 18 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 19 | internal struct XCONTENT_SIGNATURE 20 | { 21 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x100)] 22 | public byte[] Signature; 23 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x128)] 24 | public byte[] Reserved; 25 | } 26 | 27 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 28 | internal struct XCONTENT_LICENSE 29 | { 30 | public ulong LicenseeId; 31 | public uint LicenseBits; 32 | public uint LicenseFlags; 33 | } 34 | 35 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 36 | internal struct XCONTENT_HEADER 37 | { 38 | public uint SignatureType; 39 | public XCONTENT_SIGNATURE Signature; 40 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)] 41 | public XCONTENT_LICENSE[] LicenseDescriptors; 42 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] 43 | public byte[] ContentId; 44 | public uint SizeOfHeaders; 45 | } 46 | 47 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 48 | internal struct XEX_EXECUTION_ID 49 | { 50 | public uint MediaId; 51 | public uint Version; 52 | public uint BaseVersion; 53 | public uint TitleId; 54 | public byte Platform; 55 | public byte ExecutableType; 56 | public byte DiscNum; 57 | public byte DiscsInSet; 58 | public uint SaveGameID; 59 | } 60 | 61 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 62 | internal struct SVOD_DEVICE_DESCRIPTOR 63 | { 64 | public byte DescriptorLength; 65 | public byte BlockCacheElementCount; 66 | public byte WorkerThreadProcessor; 67 | public byte WorkerThreadPriority; 68 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] 69 | public byte[] FirstFragmentHashEntry; 70 | public byte Features; 71 | public byte NumberOfDataBlocks2; 72 | public byte NumberOfDataBlocks1; 73 | public byte NumberOfDataBlocks0; 74 | public byte StartingDataBlock0; 75 | public byte StartingDataBlock1; 76 | public byte StartingDataBlock2; 77 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x5)] 78 | public byte[] Reserved; 79 | } 80 | 81 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 82 | internal struct XCONTENT_METADATA_MEDIA_DATA 83 | { 84 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)] 85 | public byte[] SeriesId; 86 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)] 87 | public byte[] SeasonId; 88 | public ushort SeasonNumber; 89 | public ushort EpisodeNumber; 90 | } 91 | 92 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 93 | internal struct StringType 94 | { 95 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x100)] 96 | public byte[] Value; 97 | } 98 | 99 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 100 | internal struct XCONTENT_METADATA 101 | { 102 | public uint ContentType; 103 | public uint ContentMetadataVersion; 104 | public long ContentSize; 105 | public XEX_EXECUTION_ID ExecutionId; 106 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x5)] 107 | public byte[] ConsoleId; 108 | public long Creator; 109 | public SVOD_DEVICE_DESCRIPTOR SvodVolumeDescriptor; 110 | public uint DataFiles; 111 | public long DataFilesSize; 112 | public uint VolumeType; 113 | public long OnlineCreator; 114 | public uint Category; 115 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x20)] 116 | public byte[] Reserved2; 117 | public XCONTENT_METADATA_MEDIA_DATA Data; 118 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] 119 | public byte[] DeviceId; 120 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x9)] 121 | public StringType[] DisplayName; 122 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x9)] 123 | public StringType[] Description; 124 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x80)] 125 | public byte[] Publisher; 126 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x80)] 127 | public byte[] TitleName; 128 | public byte Flags; 129 | public uint ThumbnailSize; 130 | public uint TitleThumbnailSize; 131 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3D00)] 132 | public byte[] Thumbnail; 133 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3)] 134 | public StringType[] DisplayNameEx; 135 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3D00)] 136 | public byte[] TitleThumbnail; 137 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3)] 138 | public StringType[] DescriptionEx; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/Models/XexStructs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace XboxToolkit.Internal.Models 4 | { 5 | 6 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 7 | internal struct XexHeader 8 | { 9 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] 10 | public byte[] Magic; 11 | 12 | public uint ModuleFlags; 13 | 14 | public uint SizeOfHeaders; 15 | 16 | public uint SizeOfDiscardableHeaders; 17 | 18 | public uint SecurityInfo; 19 | 20 | public uint HeaderDirectoryEntryCount; 21 | } 22 | 23 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 24 | internal struct HvImageInfo 25 | { 26 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x100)] 27 | public byte[] Signature; 28 | 29 | public uint InfoSize; 30 | 31 | public uint ImageFlags; 32 | 33 | public uint LoadAddress; 34 | 35 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] 36 | public byte[] ImageHash; 37 | 38 | public uint ImportTableCoun; 39 | 40 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] 41 | public byte[] ImportDigest; 42 | 43 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)] 44 | public byte[] MediaID; 45 | 46 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)] 47 | public byte[] ImageKey; 48 | 49 | public uint ExportTableAddress; 50 | 51 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] 52 | public byte[] HeaderHash; 53 | 54 | public uint GameRegion; 55 | } 56 | 57 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 58 | internal struct XexSecurityInfo 59 | { 60 | public uint Size; 61 | 62 | public uint ImageSize; 63 | 64 | public HvImageInfo ImageInfo; 65 | 66 | public uint AllowedMediaTypes; 67 | 68 | public uint PageDescriptorCount; 69 | } 70 | 71 | [StructLayout(LayoutKind.Explicit, Pack = 1)] 72 | internal struct XexExecution 73 | { 74 | [FieldOffset(0)] 75 | public uint MediaId; 76 | 77 | [FieldOffset(4)] 78 | public uint Version; 79 | 80 | [FieldOffset(8)] 81 | public uint BaseVersion; 82 | 83 | [FieldOffset(12)] 84 | public uint TitleId; 85 | 86 | [FieldOffset(12)] 87 | public ushort PublisherId; 88 | 89 | [FieldOffset(14)] 90 | public ushort GameId; 91 | 92 | [FieldOffset(16)] 93 | public byte Platform; 94 | 95 | [FieldOffset(17)] 96 | public byte ExecutableType; 97 | 98 | [FieldOffset(18)] 99 | public byte DiscNum; 100 | 101 | [FieldOffset(19)] 102 | public byte DiscTotal; 103 | 104 | [FieldOffset(20)] 105 | public uint SaveGameID; 106 | } 107 | 108 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 109 | internal struct XexFileDataDescriptor 110 | { 111 | public uint Size; 112 | 113 | public ushort Flags; 114 | 115 | public ushort Format; 116 | } 117 | 118 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 119 | internal struct XexDataDescriptor 120 | { 121 | public uint Size; 122 | 123 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] 124 | public byte[] DataDigest; 125 | } 126 | 127 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 128 | internal struct XexCompressedDescriptor 129 | { 130 | public uint WindowSize; 131 | 132 | public uint Size; 133 | 134 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] 135 | public byte[] DataDigest; 136 | } 137 | 138 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 139 | internal struct XexRawDescriptor 140 | { 141 | public uint DataSize; 142 | 143 | public uint ZeroSize; 144 | } 145 | 146 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 147 | internal struct XexHeaderSectionTable 148 | { 149 | public uint Size; 150 | } 151 | 152 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 153 | internal struct XexHeaderSectionEntry 154 | { 155 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x8)] 156 | public byte[] SectionName; 157 | 158 | public uint VirtualAddress; 159 | 160 | public uint VirtualSize; 161 | } 162 | 163 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 164 | internal struct XdbfHeader 165 | { 166 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x4)] 167 | public byte[] Magic; 168 | 169 | public uint Version; 170 | 171 | public uint EntryTableLen; 172 | 173 | public uint EntryCount; 174 | 175 | public uint freeMemTablLen; 176 | 177 | public uint freeMemTablEntryCount; 178 | } 179 | 180 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 181 | internal struct XdbfEntry 182 | { 183 | public ushort Type; 184 | 185 | public uint Identifier1; 186 | 187 | public uint Identifier2; 188 | 189 | public uint Offset; 190 | 191 | public uint Length; 192 | } 193 | 194 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 195 | internal struct XsrcHeader 196 | { 197 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x4)] 198 | public byte[] Magic; 199 | 200 | public uint Version; 201 | 202 | public uint Size; 203 | 204 | public uint FileNameLen; 205 | } 206 | 207 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 208 | internal struct XsrcBody 209 | { 210 | public uint DecompressedSize; 211 | 212 | public uint CompressedSize; 213 | } 214 | 215 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 216 | internal struct XstrHeader 217 | { 218 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x4)] 219 | public byte[] Magic; 220 | 221 | public uint Version; 222 | 223 | public uint Size; 224 | 225 | public ushort EntryCount; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /XboxToolkit/CCIContainerReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using XboxToolkit.Interface; 5 | using XboxToolkit.Internal.Decoders; 6 | using XboxToolkit.Internal.Models; 7 | 8 | namespace XboxToolkit 9 | { 10 | public class CCIContainerReader : ContainerReader, IDisposable 11 | { 12 | private string mFilePath; 13 | private int mMountCount; 14 | private SectorDecoder? mSectorDecoder; 15 | private bool mDisposed; 16 | 17 | public CCIContainerReader(string filePath) 18 | { 19 | mFilePath = filePath; 20 | mMountCount = 0; 21 | } 22 | 23 | public override SectorDecoder GetDecoder() 24 | { 25 | if (mSectorDecoder == null) 26 | { 27 | throw new Exception("Container not mounted."); 28 | } 29 | return mSectorDecoder; 30 | } 31 | 32 | public static bool IsCCI(string filePath) 33 | { 34 | if (File.Exists(filePath) == false) 35 | { 36 | return false; 37 | } 38 | return Path.GetExtension(filePath).Equals(".cci", StringComparison.CurrentCultureIgnoreCase); 39 | } 40 | 41 | public override bool TryMount() 42 | { 43 | try 44 | { 45 | if (mMountCount > 0) 46 | { 47 | mMountCount++; 48 | return true; 49 | } 50 | 51 | if (IsCCI(mFilePath) == false) 52 | { 53 | return false; 54 | } 55 | 56 | var fileSlices = ContainerUtility.GetSlicesFromFile(mFilePath); 57 | 58 | var cciDetails = new List(); 59 | 60 | var sectorCount = 0L; 61 | foreach (var fileSlice in fileSlices) 62 | { 63 | 64 | using (var fileStream = new FileStream(fileSlice, FileMode.Open, FileAccess.Read, FileShare.Read)) 65 | using (var binaryReader = new BinaryReader(fileStream)) 66 | { 67 | var header = binaryReader.ReadUInt32(); 68 | if (header != 0x4D494343) 69 | { 70 | throw new IOException("Invalid magic value in cci header."); 71 | } 72 | 73 | uint headerSize = binaryReader.ReadUInt32(); 74 | if (headerSize != 32) 75 | { 76 | throw new IOException("Invalid header size in cci header."); 77 | } 78 | 79 | ulong uncompressedSize = binaryReader.ReadUInt64(); 80 | 81 | ulong indexOffset = binaryReader.ReadUInt64(); 82 | 83 | uint blockSize = binaryReader.ReadUInt32(); 84 | if (blockSize != 2048) 85 | { 86 | throw new IOException("Invalid block size in cci header."); 87 | } 88 | 89 | byte version = binaryReader.ReadByte(); 90 | if (version != 1) 91 | { 92 | throw new IOException("Invalid version in cci header."); 93 | } 94 | 95 | byte indexAlignment = binaryReader.ReadByte(); 96 | if (indexAlignment != 2) 97 | { 98 | throw new IOException("Invalid index alignment in cci header."); 99 | } 100 | 101 | var sectors = (int)(uncompressedSize / blockSize); 102 | var entries = sectors + 1; 103 | 104 | fileStream.Position = (long)indexOffset; 105 | 106 | var indexInfo = new List(); 107 | for (var i = 0; i < entries; i++) 108 | { 109 | var index = binaryReader.ReadUInt32(); 110 | var position = (ulong)(index & 0x7FFFFFFF) << indexAlignment; 111 | indexInfo.Add(new CCIIndex 112 | { 113 | Value = position, 114 | LZ4Compressed = (index & 0x80000000) > 0 115 | }); 116 | } 117 | 118 | var cciDetail = new CCIDetail 119 | { 120 | Stream = new FileStream(fileSlice, FileMode.Open, FileAccess.Read, FileShare.Read), 121 | IndexInfo = indexInfo.ToArray(), 122 | StartSector = sectorCount, 123 | EndSector = sectorCount + sectors - 1 124 | }; 125 | cciDetails.Add(cciDetail); 126 | sectorCount += sectors; 127 | } 128 | } 129 | 130 | mSectorDecoder = new CCISectorDecoder(cciDetails.ToArray()); 131 | if (mSectorDecoder.Init() == false) 132 | { 133 | return false; 134 | } 135 | 136 | mMountCount++; 137 | return true; 138 | } 139 | catch (Exception ex) 140 | { 141 | System.Diagnostics.Debug.Print(ex.ToString()); 142 | return false; 143 | } 144 | } 145 | 146 | public override void Dismount() 147 | { 148 | if (mMountCount == 0) 149 | { 150 | return; 151 | } 152 | mMountCount--; 153 | } 154 | 155 | public override int GetMountCount() 156 | { 157 | return mMountCount; 158 | } 159 | 160 | public override void Dispose() 161 | { 162 | Dispose(true); 163 | GC.SuppressFinalize(this); 164 | } 165 | 166 | protected virtual void Dispose(bool disposing) 167 | { 168 | if (mDisposed == false) 169 | { 170 | if (disposing) 171 | { 172 | mSectorDecoder?.Dispose(); 173 | } 174 | mDisposed = true; 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XboxToolkit 2 | 3 | A comprehensive .NET library for working with Xbox 360 and Xbox Original game containers, executables, and metadata. 4 | 5 | ## Overview 6 | 7 | XboxToolkit provides a complete set of tools for reading, extracting, and creating Xbox game container formats. The library supports multiple container types, XEX file processing, and marketplace metadata integration. 8 | 9 | ## Features 10 | 11 | ### Container Support 12 | - **ISO Containers**: Read Xbox Original and Xbox 360 ISO files 13 | - **CCI Containers**: Read and decode compressed container images (LZ4 compressed) 14 | - **GOD Containers**: Read Game on Demand (GOD) container formats 15 | - **Multi-slice Support**: Automatic handling of split container files 16 | 17 | ### XEX File Processing 18 | - Extract metadata from XEX executables 19 | - Decrypt retail and devkit XEX files 20 | - Decompress compressed XEX data 21 | - Extract game information (Title ID, Media ID, version, regions) 22 | - Extract thumbnails and localized strings 23 | - Parse XDBF (Xbox Data Base Format) structures 24 | - Extract XSRC (Xbox Source) XML data 25 | 26 | ### Container Building 27 | - Create ISO files from folder structures 28 | - Support for both Xbox Original and Xbox 360 ISO formats 29 | - Progress reporting for long operations 30 | 31 | ### Marketplace Integration 32 | - Fetch game metadata from Xbox Marketplace 33 | - Download game images and thumbnails 34 | - Parse marketplace XML data 35 | 36 | ### Image Processing & Texture Conversion 37 | - **XPR Texture Support**: Convert Xbox texture files (XPR) to JPEG format 38 | - **DDS Support**: Convert DirectDraw Surface (DDS) files to PNG format 39 | - **DXT Compression**: Support for DXT1, DXT3, and DXT5 texture compression formats 40 | - **Automatic Format Detection**: Automatically detects and converts various image formats 41 | 42 | ### Xbox Original (XBE) Support 43 | - Extract and replace images from XBE executables (logo, title, save images) 44 | - Extract certificate information from XBE files 45 | - Replace certificate information in XBE files 46 | - Modify XBE title images 47 | 48 | ### BIOS Logo Processing 49 | - Decode Xbox BIOS boot logos to PNG format 50 | - Encode PNG images to Xbox BIOS logo format 51 | - Support for custom boot logo creation 52 | 53 | ### Additional Features 54 | - XGD (Xbox Game Disc) information extraction 55 | - Sector-level decoding and reading 56 | - Cross-platform native library support (Windows, Linux, macOS) 57 | 58 | ## Installation 59 | 60 | ### NuGet Package 61 | 62 | ```bash 63 | Install-Package XboxToolkit 64 | ``` 65 | 66 | The package includes native libraries for Windows (x64), Linux (x64), and macOS. 67 | 68 | ## Requirements 69 | 70 | - .NET 6.0 or higher 71 | - Native libraries are included in the NuGet package 72 | 73 | ## Usage Examples 74 | 75 | ### Reading an ISO Container 76 | 77 | ```csharp 78 | using XboxToolkit; 79 | 80 | // Auto-detect container type 81 | if (ContainerUtility.TryAutoDetectContainerType("game.iso", out var containerReader)) 82 | { 83 | if (containerReader.TryMount()) 84 | { 85 | // Extract files from container 86 | ContainerUtility.ExtractFilesFromContainer(containerReader, "output_folder"); 87 | 88 | containerReader.Dismount(); 89 | containerReader.Dispose(); 90 | } 91 | } 92 | ``` 93 | 94 | ### Reading a CCI Container 95 | 96 | ```csharp 97 | var cciReader = new CCIContainerReader("game.cci"); 98 | if (cciReader.TryMount()) 99 | { 100 | var decoder = cciReader.GetDecoder(); 101 | // Use decoder to read sectors... 102 | cciReader.Dismount(); 103 | cciReader.Dispose(); 104 | } 105 | ``` 106 | 107 | ### Extracting XEX Metadata 108 | 109 | ```csharp 110 | byte[] xexData = File.ReadAllBytes("default.xex"); 111 | if (XexUtility.TryExtractXexMetaData(xexData, out var metaData)) 112 | { 113 | Console.WriteLine($"Title: {metaData.TitleName}"); 114 | Console.WriteLine($"Title ID: {metaData.TitleId:X8}"); 115 | Console.WriteLine($"Publisher: {metaData.Publisher}"); 116 | Console.WriteLine($"Developer: {metaData.Developer}"); 117 | Console.WriteLine($"Version: {metaData.Version}"); 118 | } 119 | ``` 120 | 121 | ### Creating an ISO from Folder 122 | 123 | ```csharp 124 | // Create Xbox 360 ISO 125 | ContainerUtility.ConvertFolderToISO( 126 | "input_folder", 127 | ISOFormat.Xbox360, 128 | "output.iso", 129 | splitPoint: 0, 130 | progress: (percent) => Console.WriteLine($"Progress: {percent:P}") 131 | ); 132 | ``` 133 | 134 | ### Fetching Marketplace Metadata 135 | 136 | ```csharp 137 | uint titleId = 0x41560817; // Example Title ID 138 | string marketplaceUrl = MarketplaceUtility.GetMarketPlaceUrl(titleId); 139 | // Use the URL to fetch additional metadata 140 | ``` 141 | 142 | ### Converting XPR Textures 143 | 144 | ```csharp 145 | byte[] xprData = File.ReadAllBytes("texture.xpr"); 146 | if (XprUtility.ConvertXprToJpeg(xprData, out var jpegData)) 147 | { 148 | File.WriteAllBytes("texture.jpg", jpegData); 149 | } 150 | ``` 151 | 152 | ### Converting DDS Files 153 | 154 | ```csharp 155 | byte[] ddsData = File.ReadAllBytes("texture.dds"); 156 | if (XprUtility.ConvertDdsToPng(ddsData, out var pngData)) 157 | { 158 | File.WriteAllBytes("texture.png", pngData); 159 | } 160 | ``` 161 | 162 | ### Working with XBE Files 163 | 164 | ```csharp 165 | byte[] xbeData = File.ReadAllBytes("default.xbe"); 166 | 167 | // Extract certificate information 168 | if (XbeUtility.TryGetXbeCert(xbeData, out var cert)) 169 | { 170 | Console.WriteLine($"Title: {cert.TitleName}"); 171 | Console.WriteLine($"Title ID: {cert.TitleId:X8}"); 172 | } 173 | 174 | // Extract title image 175 | if (XbeUtility.TryGetXbeImage(xbeData, XbeUtility.ImageType.TitleImage, out var imageData)) 176 | { 177 | File.WriteAllBytes("title_image.png", imageData); 178 | } 179 | 180 | // Replace title image 181 | byte[] newImage = File.ReadAllBytes("new_title.png"); 182 | XbeUtility.TryReplaceXbeTitleImage(xbeData, newImage); 183 | ``` 184 | 185 | ### Processing BIOS Logos 186 | 187 | ```csharp 188 | var biosLogo = new BiosLogoUtility(); 189 | 190 | // Decode BIOS logo to PNG 191 | byte[] logoData = File.ReadAllBytes("bios_logo.bin"); 192 | biosLogo.DecodeLogoImage(logoData, out var pngData); 193 | File.WriteAllBytes("bios_logo.png", pngData); 194 | 195 | // Encode PNG to BIOS logo format 196 | byte[] pngImage = File.ReadAllBytes("custom_logo.png"); 197 | biosLogo.EncodeLogoImage(pngImage, width: 100, height: 17, out var encodedLogo); 198 | File.WriteAllBytes("custom_bios_logo.bin", encodedLogo); 199 | ``` 200 | 201 | ## Supported Formats 202 | 203 | ### Container Formats 204 | - **ISO**: Xbox Original and Xbox 360 disc images 205 | - **CCI**: Compressed Container Image (LZ4 compressed) 206 | - **GOD**: Game on Demand container format 207 | 208 | ### Executable Formats 209 | - **XEX**: Xbox 360 executable format 210 | - **XBE**: Xbox Original executable format 211 | 212 | ### Image & Texture Formats 213 | - **XPR**: Xbox texture format (converts to JPEG) 214 | - **DDS**: DirectDraw Surface format (converts to PNG) 215 | - **DXT1/DXT3/DXT5**: Compressed texture formats 216 | - **PNG/JPEG**: Standard image formats for output 217 | 218 | ### Data Formats 219 | - **XDBF**: Xbox Data Base Format 220 | - **XSRC**: Xbox Source XML format 221 | - **BIOS Logo**: Xbox BIOS boot logo format 222 | 223 | ## Project Structure 224 | 225 | The main library is located in the `XboxToolkit` directory. This project focuses on the core library functionality and does not include test projects. 226 | 227 | ## License 228 | 229 | This project is licensed under the GPL-3.0-only License. 230 | 231 | ## Authors 232 | 233 | - **EqUiNoX** - Team Resurgent 234 | 235 | ## Repository 236 | 237 | - **GitHub**: [https://github.com/Team-Resurgent/XboxToolkit](https://github.com/Team-Resurgent/XboxToolkit) 238 | 239 | ## Contributing 240 | 241 | Contributions are welcome! Please feel free to submit a Pull Request. 242 | 243 | ## Acknowledgments 244 | 245 | This library uses the following dependencies: 246 | - **K4os.Compression.LZ4** (v1.3.8) - LZ4 compression support 247 | - **LibDeflate.NET** (v1.19.0) - GZIP decompression support 248 | - **SixLabors.ImageSharp** (v3.1.12) - Image processing and format conversion 249 | -------------------------------------------------------------------------------- /XboxToolkit/XbeUtility.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp; 2 | using SixLabors.ImageSharp.PixelFormats; 3 | using SixLabors.ImageSharp.Processing; 4 | using System; 5 | using System.IO; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | using XboxToolkit.Internal; 9 | using XboxToolkit.Internal.Xbe; 10 | using XboxToolkit.Models.Xbe; 11 | 12 | namespace XboxToolkit 13 | { 14 | public static class XbeUtility 15 | { 16 | public enum ImageType 17 | { 18 | LogoImage, 19 | TitleImage, 20 | SaveImage 21 | } 22 | 23 | public static bool ReplaceCertInfo(byte[]? attach, byte[]? donor, string xbeTitle, out byte[]? output) 24 | { 25 | output = null; 26 | 27 | if (attach == null || donor == null) 28 | { 29 | return false; 30 | } 31 | 32 | using var attachStream = new MemoryStream(attach); 33 | using var attachReader = new BinaryReader(attachStream); 34 | var attachHeader = StructUtility.ByteToType(attachReader); 35 | var atatchBaseAddress = attachHeader.Base; 36 | var atatchCertAddress = attachHeader.Certificate_Addr; 37 | 38 | using var donorStream = new MemoryStream(donor); 39 | using var donorReader = new BinaryReader(donorStream); 40 | var donorHeader = StructUtility.ByteToType(donorReader); 41 | var donorBaseAddress = donorHeader.Base; 42 | var donorCertAddress = donorHeader.Certificate_Addr; 43 | 44 | Array.Fill(donor, 0, (int)(donorCertAddress - donorBaseAddress) + 12, 80); 45 | var title = Encoding.Unicode.GetBytes(xbeTitle); 46 | Array.Copy(title, 0, donor, (donorCertAddress - donorBaseAddress) + 12, Math.Min(80, title.Length)); 47 | 48 | output = new byte[attach.Length]; 49 | Array.Copy(attach, output, attach.Length); 50 | Array.Copy(donor, donorCertAddress - donorBaseAddress, output, atatchCertAddress - atatchBaseAddress, Marshal.SizeOf(typeof(XbeCertificate))); 51 | 52 | // Force diff version for TU's to work 53 | output[(atatchCertAddress - atatchBaseAddress) + 175] = (byte)(output[(atatchCertAddress - atatchBaseAddress) + 175] | 0x80); 54 | 55 | return true; 56 | } 57 | 58 | public static bool TryGetXbeCert(byte[]? input, out XbeCertificate? output) 59 | { 60 | output = null; 61 | 62 | if (input == null) 63 | { 64 | return false; 65 | } 66 | 67 | try 68 | { 69 | using var stream = new MemoryStream(input); 70 | using var reader = new BinaryReader(stream); 71 | var header = StructUtility.ByteToType(reader); 72 | 73 | var baseAddress = header.Base; 74 | var certAddress = header.Certificate_Addr; 75 | stream.Position = certAddress - baseAddress; 76 | 77 | output = StructUtility.ByteToType(reader); 78 | if (output != null) 79 | { 80 | output.Version &= 0x7fffffff; 81 | } 82 | return true; 83 | } 84 | catch 85 | { 86 | 87 | } 88 | return false; 89 | } 90 | 91 | public static bool TryReplaceXbeTitleImage(byte[]? input, byte[]? image) 92 | { 93 | if (input == null) 94 | { 95 | return false; 96 | } 97 | 98 | if (image == null) 99 | { 100 | return false; 101 | } 102 | 103 | using var memoryStream = new MemoryStream(image); 104 | using var icon = Image.Load(memoryStream); 105 | icon.Mutate(m => m.Resize(128, 128)); 106 | icon.Mutate(m => m.Flip(FlipMode.Vertical)); 107 | icon.Mutate(m => m.BackgroundColor(Color.Black)); 108 | using var tempImage = icon.CloneAs(); 109 | var iconBuffer = new byte[49152]; 110 | tempImage.CopyPixelDataTo(iconBuffer); 111 | 112 | using var stream = new MemoryStream(input); 113 | using var reader = new BinaryReader(stream); 114 | var header = StructUtility.ByteToType(reader); 115 | 116 | var baseAddress = header.Base; 117 | var certAddress = header.Certificate_Addr; 118 | stream.Position = certAddress - baseAddress; 119 | 120 | var cert = StructUtility.ByteToType(reader); 121 | if (cert == null) 122 | { 123 | return false; 124 | } 125 | 126 | var bitmapAddress = header.Logo_Bitmap_Addr; 127 | var bitmapSize = header.Logo_Bitmap_Size; 128 | stream.Position = bitmapAddress - baseAddress; 129 | 130 | var title_Name = UnicodeHelper.GetUnicodeString(cert.Title_Name); 131 | 132 | if (header.Sections > 0) 133 | { 134 | var sectionAddress = header.Section_Headers_Addr; 135 | 136 | for (int i = 0; i < header.Sections; i++) 137 | { 138 | stream.Position = (sectionAddress - baseAddress) + (i * Marshal.SizeOf(typeof(XbeSectionHeader))); 139 | var section = StructUtility.ByteToType(reader); 140 | 141 | var name = ""; 142 | if (section.Section_Name_Addr != 0) 143 | { 144 | stream.Position = section.Section_Name_Addr - baseAddress; 145 | 146 | var sectionNameBytes = reader.ReadBytes(20); 147 | name = UnicodeHelper.GetUtf8String(sectionNameBytes); 148 | } 149 | 150 | var rawaddress = section.Raw_Addr; 151 | var rawsize = section.Sizeof_Raw; 152 | stream.Position = rawaddress; 153 | 154 | if (name == "$$XTIMAGE" && rawsize == 49206) 155 | { 156 | Array.Copy(iconBuffer, 0, input, rawaddress + (49206 - 49152), 49152); 157 | return true; 158 | } 159 | } 160 | } 161 | 162 | return false; 163 | } 164 | 165 | public static bool TryGetXbeImage(byte[]? input, ImageType imageType, out byte[]? output) 166 | { 167 | output = null; 168 | 169 | if (input == null) 170 | { 171 | return false; 172 | } 173 | 174 | using var stream = new MemoryStream(input); 175 | using var reader = new BinaryReader(stream); 176 | var header = StructUtility.ByteToType(reader); 177 | 178 | var baseAddress = header.Base; 179 | var certAddress = header.Certificate_Addr; 180 | stream.Position = certAddress - baseAddress; 181 | 182 | var cert = StructUtility.ByteToType(reader); 183 | if (cert == null) 184 | { 185 | return false; 186 | } 187 | 188 | var bitmapAddress = header.Logo_Bitmap_Addr; 189 | var bitmapSize = header.Logo_Bitmap_Size; 190 | stream.Position = bitmapAddress - baseAddress; 191 | 192 | if (imageType == ImageType.LogoImage) 193 | { 194 | output = reader.ReadBytes((int)bitmapSize); 195 | return true; 196 | } 197 | 198 | var title_Name = UnicodeHelper.GetUnicodeString(cert.Title_Name); 199 | 200 | if (header.Sections > 0) 201 | { 202 | var sectionAddress = header.Section_Headers_Addr; 203 | 204 | for (int i = 0; i < header.Sections; i++) 205 | { 206 | stream.Position = (sectionAddress - baseAddress) + (i * Marshal.SizeOf(typeof(XbeSectionHeader))); 207 | var section = StructUtility.ByteToType(reader); 208 | 209 | var name = ""; 210 | if (section.Section_Name_Addr != 0) 211 | { 212 | stream.Position = section.Section_Name_Addr - baseAddress; 213 | 214 | var sectionNameBytes = reader.ReadBytes(20); 215 | name = UnicodeHelper.GetUtf8String(sectionNameBytes); 216 | } 217 | 218 | var rawaddress = section.Raw_Addr; 219 | var rawsize = section.Sizeof_Raw; 220 | stream.Position = rawaddress; 221 | 222 | if (name == "$$XTIMAGE" && imageType == ImageType.TitleImage) 223 | { 224 | output = reader.ReadBytes((int)rawsize); 225 | return true; 226 | } 227 | else if (name == "$$XSIMAGE" && imageType == ImageType.SaveImage) 228 | { 229 | output = reader.ReadBytes((int)rawsize); 230 | return true; 231 | } 232 | } 233 | } 234 | 235 | return false; 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /XboxToolkit/MarketplaceUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Xml; 6 | using XboxToolkit.Models; 7 | 8 | namespace XboxToolkit 9 | { 10 | public static class MarketplaceUtility 11 | { 12 | private static bool TryDownloadMarketplaceImage(string imageUrl, out byte[] imageData) 13 | { 14 | imageData = Array.Empty(); 15 | 16 | var retries = 10; 17 | while (retries > 0) 18 | { 19 | try 20 | { 21 | using (var client = new HttpClient()) 22 | { 23 | using (var response = client.GetAsync(imageUrl).GetAwaiter().GetResult()) 24 | { 25 | if (response.IsSuccessStatusCode) 26 | { 27 | var responseContent = response.Content; 28 | imageData = responseContent.ReadAsByteArrayAsync().GetAwaiter().GetResult(); 29 | } 30 | } 31 | } 32 | return true; 33 | } 34 | catch 35 | { 36 | Thread.Sleep(500); 37 | } 38 | retries--; 39 | } 40 | return false; 41 | } 42 | 43 | public static string GetMarketPlaceUrl(uint titleId) 44 | { 45 | const string locale = "en-US"; 46 | 47 | var parameters = new List> 48 | { 49 | new KeyValuePair( "Locale", locale ), 50 | new KeyValuePair("LegalLocale", locale ), 51 | new KeyValuePair("Store", "1" ), 52 | new KeyValuePair( "PageSize", "100" ), 53 | new KeyValuePair( "PageNum", "1" ), 54 | new KeyValuePair("DetailView", "3" ), // 5 55 | new KeyValuePair("OfferFilterLevel", "1" ), 56 | new KeyValuePair("MediaIds", "66acd000-77fe-1000-9115-d802" + titleId.ToString("X8") ), 57 | new KeyValuePair("UserTypes", "2" ), 58 | new KeyValuePair("MediaTypes", "1" ), // Xbox360 59 | new KeyValuePair("MediaTypes", "21" ), 60 | new KeyValuePair("MediaTypes", "23" ), // XBLA 61 | new KeyValuePair("MediaTypes", "37" ), // Community 62 | new KeyValuePair("MediaTypes", "46" ), 63 | }; 64 | 65 | var url = "http://catalog.xboxlive.com/Catalog/Catalog.asmx/Query?methodName=FindGames"; 66 | foreach (var parameter in parameters) 67 | { 68 | url = $"{url}&Names={parameter.Key}&Values={parameter.Value}"; 69 | } 70 | 71 | return url; 72 | } 73 | 74 | private static bool TryGetMarketPlaceXml(string marketPlaceURL, out XmlDocument marketPlaceXml) 75 | { 76 | if (string.IsNullOrEmpty(marketPlaceURL)) 77 | { 78 | throw new ArgumentException("Argument cannot be Null or Empty", "marketPlaceURL"); 79 | } 80 | 81 | marketPlaceXml = new XmlDocument(); 82 | 83 | var retries = 10; 84 | while (retries > 0) 85 | { 86 | try 87 | { 88 | marketPlaceXml.Load(marketPlaceURL); 89 | return true; 90 | } 91 | catch 92 | { 93 | Thread.Sleep(500); 94 | } 95 | retries--; 96 | } 97 | return false; 98 | } 99 | 100 | public static bool TryProcessMarketplaceTitle(uint titleId, bool downloadImages, out MarketplaceMetaData marketplaceMetaData) 101 | { 102 | marketplaceMetaData = new MarketplaceMetaData(); 103 | 104 | try 105 | { 106 | var marketPlaceUrl = GetMarketPlaceUrl(titleId); 107 | if (TryGetMarketPlaceXml(marketPlaceUrl, out var marketPlaceXML) == false) 108 | { 109 | return false; 110 | } 111 | 112 | var root = marketPlaceXML.DocumentElement; 113 | if (root == null) 114 | { 115 | return false; 116 | } 117 | 118 | var xmlnsm = new XmlNamespaceManager(marketPlaceXML.NameTable); 119 | xmlnsm.AddNamespace("default", "http://www.w3.org/2005/Atom"); 120 | xmlnsm.AddNamespace("live", "http://www.live.com/marketplace"); 121 | 122 | if (root.SelectSingleNode("live:totalItems/text()", xmlnsm)?.Value == "0") 123 | { 124 | return false; 125 | } 126 | 127 | marketplaceMetaData.Developer = root.SelectSingleNode("default:entry/live:media/live:developer/text()", xmlnsm)?.Value ?? string.Empty; 128 | marketplaceMetaData.Title = root.SelectSingleNode("default:entry/default:title/text()", xmlnsm)?.Value ?? string.Empty; 129 | marketplaceMetaData.Publisher = root.SelectSingleNode("default:entry/live:media/live:publisher/text()", xmlnsm)?.Value ?? string.Empty; 130 | marketplaceMetaData.Description = root.SelectSingleNode("default:entry/live:media/live:reducedDescription/text()", xmlnsm)?.Value ?? string.Empty; 131 | 132 | var xmlNodeList = root.SelectNodes("default:entry/live:categories/live:category", xmlnsm); 133 | if (xmlNodeList == null) 134 | { 135 | return false; 136 | } 137 | 138 | var genreMap = new Dictionary 139 | { 140 | { 0, new Genre(0, "Unknown") }, 141 | { 3001, new Genre(3001, "Other") }, 142 | { 3002, new Genre(3002, "Action & Adventure") }, 143 | { 3005, new Genre(3005, "Family") }, 144 | { 3006, new Genre(3006, "Fighting") }, 145 | { 3007, new Genre(3007, "Music") }, 146 | { 3008, new Genre(3008, "Platformer") }, 147 | { 3009, new Genre(3009, "Racing & Flying") }, 148 | { 3010, new Genre(3010, "Role Playing") }, 149 | { 3011, new Genre(3011, "Shooter") }, 150 | { 3012, new Genre(3012, "Strategy & Simulation") }, 151 | { 3013, new Genre(3013, "Sports & Recreation") }, 152 | { 3018, new Genre(3018, "Board & Card") }, 153 | { 3019, new Genre(3019, "Classics") }, 154 | { 3022, new Genre(3022, "Puzzle & Trivia") } 155 | }; 156 | 157 | foreach (XmlNode xmlNode in xmlNodeList) 158 | { 159 | var node = xmlNode.SelectSingleNode("live:categoryId/text()", xmlnsm); 160 | if (node == null) 161 | { 162 | continue; 163 | } 164 | if (uint.TryParse(node.Value, out var categoryId) && categoryId > 3000) 165 | { 166 | if (genreMap.ContainsKey(categoryId) == false) 167 | { 168 | continue; 169 | } 170 | marketplaceMetaData.Genre = (GenreType)categoryId; 171 | break; 172 | } 173 | } 174 | 175 | if (downloadImages == true) 176 | { 177 | var titleNode = root.SelectSingleNode("default:entry/live:images/live:image[live:imageMediaType=14 and live:size=14 and (live:relationshipType=23 or live:relationshipType=15)]/live:fileUrl/text()", xmlnsm); 178 | if (titleNode?.Value != null) 179 | { 180 | if (TryDownloadMarketplaceImage(titleNode.Value, out var titleImage)) 181 | { 182 | marketplaceMetaData.TitleImage = titleImage; 183 | } 184 | } 185 | 186 | var backgroundNode = root.SelectSingleNode("default:entry/live:images/live:image[live:imageMediaType=14 and live:size=22 and live:relationshipType=25]/live:fileUrl/text()", xmlnsm); 187 | if (backgroundNode?.Value != null) 188 | { 189 | if (TryDownloadMarketplaceImage(backgroundNode.Value, out var backgroundImage)) 190 | { 191 | marketplaceMetaData.BackgroundImage = backgroundImage; 192 | } 193 | } 194 | 195 | var bannerNode = root.SelectSingleNode("default:entry/live:images/live:image[live:imageMediaType=14 and live:size=15 and live:relationshipType=27]/live:fileUrl/text()", xmlnsm); 196 | if (bannerNode?.Value != null) 197 | { 198 | if (TryDownloadMarketplaceImage(bannerNode.Value, out var bannerImage)) 199 | { 200 | marketplaceMetaData.BannerImage = bannerImage; 201 | } 202 | } 203 | 204 | var boxartNode = root.SelectSingleNode("default:entry/live:images/live:image[live:imageMediaType=14 and live:size=23 and live:relationshipType=33]/live:fileUrl/text()", xmlnsm); 205 | if (boxartNode?.Value != null) 206 | { 207 | if (TryDownloadMarketplaceImage(boxartNode.Value, out var boxArtImage)) 208 | { 209 | marketplaceMetaData.BoxArtImage = boxArtImage; 210 | } 211 | } 212 | } 213 | 214 | return true; 215 | } 216 | catch 217 | { 218 | return false; 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /XboxToolkitTest/DirectoryStructureTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using XboxToolkit; 6 | using XboxToolkit.Interface; 7 | 8 | namespace XboxToolkitTest 9 | { 10 | internal static class DirectoryStructureTest 11 | { 12 | public static void TestDirectoryStructure() 13 | { 14 | Console.WriteLine("=== Testing Directory Structure ==="); 15 | Console.WriteLine(); 16 | 17 | // Create a simple test ISO with known files 18 | var testFolder = Path.Combine(Path.GetTempPath(), "XboxToolkitTest_" + Guid.NewGuid().ToString("N").Substring(0, 8)); 19 | Directory.CreateDirectory(testFolder); 20 | 21 | try 22 | { 23 | // Create test files 24 | File.WriteAllText(Path.Combine(testFolder, "file1.txt"), "Test file 1"); 25 | File.WriteAllText(Path.Combine(testFolder, "file2.txt"), "Test file 2"); 26 | File.WriteAllText(Path.Combine(testFolder, "file3.txt"), "Test file 3"); 27 | 28 | var testIso = Path.Combine(Path.GetTempPath(), "XboxToolkitTest_" + Guid.NewGuid().ToString("N").Substring(0, 8) + ".iso"); 29 | 30 | Console.WriteLine($"Creating test ISO from: {testFolder}"); 31 | Console.WriteLine($"Output ISO: {testIso}"); 32 | Console.WriteLine(); 33 | 34 | // Create ISO 35 | var success = ContainerUtility.ConvertFolderToISO( 36 | testFolder, 37 | ISOFormat.XboxOriginal, 38 | testIso, 39 | 0, 40 | null 41 | ); 42 | 43 | if (!success) 44 | { 45 | Console.WriteLine("ERROR: Failed to create ISO"); 46 | return; 47 | } 48 | 49 | Console.WriteLine($"ISO created: {new FileInfo(testIso).Length} bytes"); 50 | Console.WriteLine(); 51 | 52 | // Try to read it back 53 | Console.WriteLine("Reading ISO structure:"); 54 | Console.WriteLine("----------------------"); 55 | 56 | using (var isoReader = new ISOContainerReader(testIso)) 57 | { 58 | if (!isoReader.TryMount()) 59 | { 60 | Console.WriteLine("ERROR: Failed to mount ISO"); 61 | return; 62 | } 63 | 64 | var decoder = isoReader.GetDecoder(); 65 | var xgdInfo = decoder.GetXgdInfo(); 66 | 67 | Console.WriteLine($"BaseSector: {xgdInfo.BaseSector}"); 68 | Console.WriteLine($"RootDirSector: {xgdInfo.RootDirSector}"); 69 | Console.WriteLine($"RootDirSize: {xgdInfo.RootDirSize}"); 70 | Console.WriteLine(); 71 | 72 | // Read root directory 73 | const uint SECTOR_SIZE = 0x800; 74 | var rootSectors = xgdInfo.RootDirSize / SECTOR_SIZE; 75 | var rootData = new byte[xgdInfo.RootDirSize]; 76 | for (var i = 0; i < rootSectors; i++) 77 | { 78 | var currentRootSector = xgdInfo.BaseSector + xgdInfo.RootDirSector + (uint)i; 79 | if (decoder.TryReadSector(currentRootSector, out var sectorData) == false) 80 | { 81 | Console.WriteLine($"ERROR: Failed to read root sector {i}"); 82 | return; 83 | } 84 | Array.Copy(sectorData, 0, rootData, i * SECTOR_SIZE, SECTOR_SIZE); 85 | } 86 | 87 | Console.WriteLine($"Root directory data: {rootData.Length} bytes"); 88 | Console.WriteLine(); 89 | 90 | // Try to read entries 91 | var treeNodes = new List<(byte[] data, uint offset, string path)> 92 | { 93 | (rootData, 0, string.Empty) 94 | }; 95 | 96 | int entryCount = 0; 97 | int errorCount = 0; 98 | 99 | while (treeNodes.Count > 0) 100 | { 101 | var (data, offset, path) = treeNodes[0]; 102 | treeNodes.RemoveAt(0); 103 | 104 | using (var stream = new MemoryStream(data)) 105 | using (var reader = new BinaryReader(stream)) 106 | { 107 | if (offset * 4 >= stream.Length) 108 | { 109 | Console.WriteLine($" WARNING: Offset {offset} * 4 = {offset * 4} >= {stream.Length}, skipping"); 110 | continue; 111 | } 112 | 113 | stream.Position = offset * 4; 114 | 115 | // Check if we can read at least 14 bytes 116 | if (stream.Position + 14 > stream.Length) 117 | { 118 | Console.WriteLine($" ERROR: Cannot read full entry at offset {offset} (position {stream.Position}, length {stream.Length})"); 119 | errorCount++; 120 | continue; 121 | } 122 | 123 | var left = reader.ReadUInt16(); 124 | var right = reader.ReadUInt16(); 125 | var sector = reader.ReadUInt32(); 126 | var size = reader.ReadUInt32(); 127 | var attribute = reader.ReadByte(); 128 | var nameLength = reader.ReadByte(); 129 | 130 | Console.WriteLine($" Entry #{entryCount + 1} at offset {offset} (byte {offset * 4}):"); 131 | Console.WriteLine($" Left: {left} (0x{left:X4}), Right: {right} (0x{right:X4})"); 132 | Console.WriteLine($" Sector: {sector}, Size: {size}"); 133 | Console.WriteLine($" Attribute: {attribute:X2}, NameLength: {nameLength}"); 134 | 135 | if (nameLength == 0 || nameLength > 255) 136 | { 137 | Console.WriteLine($" ERROR: Invalid nameLength: {nameLength}"); 138 | errorCount++; 139 | break; 140 | } 141 | 142 | if (stream.Position + nameLength > stream.Length) 143 | { 144 | Console.WriteLine($" ERROR: Cannot read filename (position {stream.Position}, need {nameLength} bytes, length {stream.Length})"); 145 | errorCount++; 146 | break; 147 | } 148 | 149 | var filenameBytes = reader.ReadBytes(nameLength); 150 | var filename = Encoding.ASCII.GetString(filenameBytes); 151 | 152 | Console.WriteLine($" Filename: '{filename}'"); 153 | 154 | // Debug: show raw bytes at this position and calculate next entry position 155 | var entryEndPosition = stream.Position; 156 | var entrySize = entryEndPosition - (offset * 4); 157 | Console.WriteLine($" Entry size: {entrySize} bytes, ends at byte {entryEndPosition}"); 158 | 159 | // Show what's at the next expected offset 160 | if (left != 0xFFFF && left != 0) 161 | { 162 | var nextOffsetByte = left * 4; 163 | if (nextOffsetByte < stream.Length) 164 | { 165 | stream.Position = nextOffsetByte; 166 | var nextRawBytes = reader.ReadBytes(Math.Min(32, (int)(stream.Length - stream.Position))); 167 | var nextHexBytes = string.Join(" ", nextRawBytes.Select(b => b.ToString("X2"))); 168 | Console.WriteLine($" Next entry (left) should be at offset {left} (byte {nextOffsetByte}): {nextHexBytes}"); 169 | } 170 | } 171 | Console.WriteLine(); 172 | 173 | if (string.IsNullOrEmpty(filename)) 174 | { 175 | Console.WriteLine($" ERROR: Empty filename at offset {offset}!"); 176 | Console.WriteLine($" nameLength={nameLength}, but filename is empty"); 177 | Console.WriteLine($" This suggests the entry wasn't written correctly or we're reading from wrong location"); 178 | errorCount++; 179 | break; 180 | } 181 | 182 | entryCount++; 183 | 184 | if (left == 0xFFFF) 185 | { 186 | Console.WriteLine($" No children (left=0xFFFF)"); 187 | continue; 188 | } 189 | 190 | if (left != 0 && left != 0xFFFF) 191 | { 192 | Console.WriteLine($" Adding left child at offset {left}"); 193 | treeNodes.Add((data, left, path)); 194 | } 195 | 196 | if (right != 0 && right != 0xFFFF) 197 | { 198 | Console.WriteLine($" Adding right child at offset {right}"); 199 | treeNodes.Add((data, right, path)); 200 | } 201 | } 202 | } 203 | 204 | Console.WriteLine("----------------------"); 205 | Console.WriteLine($"Total entries read: {entryCount}"); 206 | Console.WriteLine($"Expected entries: 3"); 207 | Console.WriteLine($"Errors: {errorCount}"); 208 | Console.WriteLine(); 209 | 210 | if (errorCount > 0) 211 | { 212 | Console.WriteLine("ERROR: Found errors reading directory structure!"); 213 | } 214 | else if (entryCount != 3) 215 | { 216 | Console.WriteLine($"ERROR: Entry count mismatch! Expected 3, got {entryCount}"); 217 | } 218 | else 219 | { 220 | Console.WriteLine("SUCCESS: All entries read correctly"); 221 | } 222 | } 223 | } 224 | finally 225 | { 226 | // Cleanup 227 | if (Directory.Exists(testFolder)) 228 | { 229 | Directory.Delete(testFolder, true); 230 | } 231 | } 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /XboxToolkit/Internal/ContainerBuilder/ContainerBuilderHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace XboxToolkit.Internal.ContainerBuilder 8 | { 9 | internal static class ContainerBuilderHelper 10 | { 11 | public static DirectoryEntry BuildDirectoryTree(string basePath, string relativePath, Dictionary directorySizes, uint sectorSize) 12 | { 13 | var rootEntry = new DirectoryEntry { Path = relativePath }; 14 | var stack = new Stack<(DirectoryEntry entry, string basePath, string relativePath)>(); 15 | stack.Push((rootEntry, basePath, relativePath)); 16 | 17 | // Phase 1: Build the directory tree structure 18 | while (stack.Count > 0) 19 | { 20 | var (dirEntry, currentBasePath, currentRelativePath) = stack.Pop(); 21 | var fullPath = string.IsNullOrEmpty(currentRelativePath) ? currentBasePath : Path.Combine(currentBasePath, currentRelativePath); 22 | 23 | // Get files in this directory 24 | var files = Directory.GetFiles(fullPath); 25 | foreach (var file in files) 26 | { 27 | var fileInfo = new FileInfo(file); 28 | var fileName = Path.GetFileName(file); 29 | var fileRelativePath = string.IsNullOrEmpty(currentRelativePath) ? fileName : Path.Combine(currentRelativePath, fileName).Replace('\\', '/'); 30 | 31 | dirEntry.Files.Add(new FileEntry 32 | { 33 | RelativePath = fileRelativePath, 34 | FullPath = file, 35 | Size = (uint)fileInfo.Length 36 | }); 37 | } 38 | 39 | // Get subdirectories and add them to stack 40 | var subdirs = Directory.GetDirectories(fullPath); 41 | foreach (var subdir in subdirs) 42 | { 43 | var dirName = Path.GetFileName(subdir); 44 | var subdirRelativePath = string.IsNullOrEmpty(currentRelativePath) ? dirName : Path.Combine(currentRelativePath, dirName).Replace('\\', '/'); 45 | var subdirEntry = new DirectoryEntry { Path = subdirRelativePath }; 46 | dirEntry.Subdirectories.Add(subdirEntry); 47 | 48 | // Push subdirectory onto stack for processing 49 | stack.Push((subdirEntry, currentBasePath, subdirRelativePath)); 50 | } 51 | } 52 | 53 | // Phase 2: Calculate directory sizes (post-order: children before parents) 54 | var directories = new List(); 55 | var sizeStack = new Stack(); 56 | sizeStack.Push(rootEntry); 57 | 58 | while (sizeStack.Count > 0) 59 | { 60 | var currentDir = sizeStack.Pop(); 61 | directories.Add(currentDir); 62 | 63 | // Push subdirectories in reverse order so they're processed in correct order 64 | for (int i = currentDir.Subdirectories.Count - 1; i >= 0; i--) 65 | { 66 | sizeStack.Push(currentDir.Subdirectories[i]); 67 | } 68 | } 69 | 70 | // Process directories in reverse order (post-order: children before parents) 71 | for (int i = directories.Count - 1; i >= 0; i--) 72 | { 73 | var dir = directories[i]; 74 | uint totalSize = 0; 75 | 76 | // Calculate size needed for all entries in this directory 77 | // Only count entries with non-empty names (matching BuildDirectoryData logic) 78 | var entries = new List<(bool isDir, string name, uint size)>(); 79 | 80 | foreach (var file in dir.Files) 81 | { 82 | var fileName = Path.GetFileName(file.RelativePath); 83 | if (!string.IsNullOrEmpty(fileName)) 84 | { 85 | entries.Add((false, fileName, file.Size)); 86 | } 87 | } 88 | 89 | foreach (var subdir in dir.Subdirectories) 90 | { 91 | var dirName = Path.GetFileName(subdir.Path); 92 | if (!string.IsNullOrEmpty(dirName)) 93 | { 94 | entries.Add((true, dirName, directorySizes[subdir.Path])); 95 | } 96 | } 97 | 98 | // Each entry is: left(2) + right(2) + sector(4) + size(4) + attribute(1) + nameLength(1) + filename 99 | // Entries must be 4-byte aligned (padded to 4-byte boundary) 100 | foreach (var entry in entries) 101 | { 102 | var nameBytes = Encoding.ASCII.GetBytes(entry.name); 103 | var entryDataSize = 2 + 2 + 4 + 4 + 1 + 1 + (uint)nameBytes.Length; 104 | var entrySize = (entryDataSize + 3) & ~3u; // Round up to 4-byte boundary 105 | totalSize += entrySize; 106 | } 107 | 108 | // Round up to sector size 109 | totalSize = Helpers.RoundToMultiple(totalSize, sectorSize); 110 | directorySizes[dir.Path] = totalSize; 111 | } 112 | 113 | return rootEntry; 114 | } 115 | 116 | public static void CollectFileEntries(DirectoryEntry directory, List fileEntries) 117 | { 118 | fileEntries.AddRange(directory.Files); 119 | foreach (var subdir in directory.Subdirectories) 120 | { 121 | CollectFileEntries(subdir, fileEntries); 122 | } 123 | } 124 | 125 | public static void AllocateDirectorySectors(DirectoryEntry directory, Dictionary directorySizes, SectorAllocator allocator, uint baseSector) 126 | { 127 | // Root directory is already allocated, skip it 128 | if (directory.Path != string.Empty) 129 | { 130 | var dirSize = directorySizes[directory.Path]; 131 | var dirSectors = Helpers.RoundToMultiple(dirSize, Constants.XGD_SECTOR_SIZE) / Constants.XGD_SECTOR_SIZE; 132 | directory.Sector = allocator.AllocateDirectorySectors(dirSectors) - baseSector; 133 | } 134 | 135 | foreach (var subdir in directory.Subdirectories) 136 | { 137 | AllocateDirectorySectors(subdir, directorySizes, allocator, baseSector); 138 | } 139 | } 140 | 141 | public static uint GetDirectorySector(string path, DirectoryEntry root, uint baseSector) 142 | { 143 | if (root.Path == path) 144 | { 145 | return root.Sector + baseSector; 146 | } 147 | 148 | foreach (var subdir in root.Subdirectories) 149 | { 150 | var result = GetDirectorySector(path, subdir, baseSector); 151 | if (result != 0) 152 | { 153 | return result; 154 | } 155 | } 156 | 157 | return 0; 158 | } 159 | 160 | public static void BuildDirectoryData(DirectoryEntry directory, List fileEntries, Dictionary directorySizes, Dictionary directoryData, uint baseSector) 161 | { 162 | var entries = new List<(bool isDir, string name, uint sector, uint size, string path)>(); 163 | 164 | foreach (var file in directory.Files) 165 | { 166 | var fileName = Path.GetFileName(file.RelativePath); 167 | if (string.IsNullOrEmpty(fileName)) 168 | { 169 | // Skip files with empty names 170 | continue; 171 | } 172 | entries.Add((false, fileName, file.Sector, file.Size, string.Empty)); 173 | } 174 | 175 | foreach (var subdir in directory.Subdirectories) 176 | { 177 | // Recursively build subdirectory data first 178 | BuildDirectoryData(subdir, fileEntries, directorySizes, directoryData, baseSector); 179 | 180 | var dirName = Path.GetFileName(subdir.Path); 181 | if (string.IsNullOrEmpty(dirName)) 182 | { 183 | // Skip directories with empty names 184 | continue; 185 | } 186 | 187 | // Add subdirectory to entries - even if it's empty, it should still be in the parent's entry list 188 | // Empty directories will have size = sector size (minimum), and their directory data will be all zeros 189 | entries.Add((true, dirName, subdir.Sector, directorySizes[subdir.Path], subdir.Path)); 190 | } 191 | 192 | // Sort entries by name for binary tree 193 | entries.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.OrdinalIgnoreCase)); 194 | 195 | // Build binary tree structure 196 | var dirSize = directorySizes[directory.Path]; 197 | var dirData = new byte[dirSize]; 198 | // Initialize to zeros to ensure uninitialized data doesn't cause issues 199 | for (int i = 0; i < dirData.Length; i++) 200 | { 201 | dirData[i] = 0; 202 | } 203 | var offset = 0u; 204 | 205 | // Build binary tree even if entries is empty (will result in all zeros, which is correct for empty directories) 206 | if (entries.Count > 0) 207 | { 208 | BuildBinaryTree(entries, dirData, ref offset, 0, entries.Count - 1, baseSector); 209 | } 210 | // If entries.Count == 0, dirData remains all zeros, which is correct for an empty directory 211 | 212 | directoryData[directory.Path] = dirData; 213 | } 214 | 215 | private static void BuildBinaryTree(List<(bool isDir, string name, uint sector, uint size, string path)> entries, byte[] dirData, ref uint offset, int start, int end, uint baseSector) 216 | { 217 | if (start > end) 218 | { 219 | return; 220 | } 221 | 222 | var currentOffset = offset; 223 | var mid = (start + end) / 2; 224 | var entry = entries[mid]; 225 | 226 | // Calculate entry size (must be 4-byte aligned for Xbox format) 227 | var entryDataSize = (uint)(2 + 2 + 4 + 4 + 1 + 1 + entry.name.Length); 228 | var entrySize = (entryDataSize + 3) & ~3u; // Round up to 4-byte boundary 229 | 230 | // Ensure name is not empty 231 | if (string.IsNullOrEmpty(entry.name)) 232 | { 233 | throw new InvalidOperationException($"Directory entry has empty name at offset {currentOffset}"); 234 | } 235 | 236 | var nameBytes = Encoding.ASCII.GetBytes(entry.name); 237 | if (nameBytes.Length == 0 || nameBytes.Length > 255) 238 | { 239 | throw new InvalidOperationException($"Directory entry name has invalid length: {nameBytes.Length}"); 240 | } 241 | 242 | // Advance offset past this entry (reserve space, already aligned) 243 | offset += entrySize; 244 | 245 | // Calculate where children will be written (offsets are in 4-byte units) 246 | ushort leftOffset = 0xFFFF; 247 | ushort rightOffset = 0xFFFF; 248 | 249 | if (start < mid) 250 | { 251 | // Left child will be written at current offset (already 4-byte aligned) 252 | leftOffset = (ushort)(offset / 4); 253 | BuildBinaryTree(entries, dirData, ref offset, start, mid - 1, baseSector); 254 | } 255 | 256 | if (mid < end) 257 | { 258 | // Right child will be written at current offset (after left subtree, already aligned) 259 | rightOffset = (ushort)(offset / 4); 260 | BuildBinaryTree(entries, dirData, ref offset, mid + 1, end, baseSector); 261 | } 262 | 263 | // Write entry at currentOffset (after children, so offsets are correct) 264 | using (var stream = new MemoryStream(dirData)) 265 | using (var writer = new BinaryWriter(stream)) 266 | { 267 | stream.Position = currentOffset; 268 | writer.Write(leftOffset); 269 | writer.Write(rightOffset); 270 | writer.Write(entry.sector); 271 | writer.Write(entry.size); 272 | writer.Write((byte)(entry.isDir ? 0x10 : 0x00)); 273 | writer.Write((byte)nameBytes.Length); 274 | writer.Write(nameBytes); 275 | 276 | // Pad to 4-byte boundary 277 | var padding = entrySize - entryDataSize; 278 | if (padding > 0) 279 | { 280 | writer.Write(new byte[padding]); 281 | } 282 | } 283 | } 284 | 285 | public static void WriteXgdHeader(byte[] sector, uint rootDirSector, uint rootDirSize) 286 | { 287 | using (var stream = new MemoryStream(sector)) 288 | using (var writer = new BinaryWriter(stream)) 289 | { 290 | var magic = Encoding.UTF8.GetBytes(Constants.XGD_IMAGE_MAGIC); 291 | writer.Write(magic); 292 | if (magic.Length < 20) 293 | { 294 | writer.Write(new byte[20 - magic.Length]); 295 | } 296 | 297 | writer.Write(rootDirSector); 298 | writer.Write(rootDirSize); 299 | writer.Write(DateTime.Now.ToFileTime()); 300 | writer.Write(new byte[0x7c8]); // Padding 301 | 302 | writer.Write(magic); // MagicTail 303 | if (magic.Length < 20) 304 | { 305 | writer.Write(new byte[20 - magic.Length]); 306 | } 307 | } 308 | } 309 | } 310 | } 311 | 312 | -------------------------------------------------------------------------------- /XboxToolkit/XprUtility.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp; 2 | using SixLabors.ImageSharp.PixelFormats; 3 | using System; 4 | using System.IO; 5 | using XboxToolkit.Internal; 6 | using XboxToolkit.Models.Dds; 7 | 8 | namespace XboxToolkit 9 | { 10 | public static class XprUtility 11 | { 12 | private static int DXT1toARGB(byte[] src, int srcOffset, Image dest, int x, int y) 13 | { 14 | // colour is in R5G6B5 format, convert to R8G8B8 15 | 16 | ushort[] colors = new ushort[2]; 17 | Rgba32[] color = new Rgba32[4]; 18 | 19 | for (var i = 0; i < 2; i++) 20 | { 21 | colors[i] = src[srcOffset++]; 22 | colors[i] |= (ushort)(src[srcOffset++] << 8); 23 | 24 | color[i].R = (byte)((colors[i] & 0xF800) >> 11); 25 | color[i].G = (byte)((colors[i] & 0x7E0) >> 5); 26 | color[i].B = (byte)(colors[i] & 0x1f); 27 | color[i].R = (byte)(color[i].R << 3 | color[i].R >> 2); 28 | color[i].G = (byte)(color[i].G << 2 | color[i].G >> 3); 29 | color[i].B = (byte)(color[i].B << 3 | color[i].B >> 2); 30 | color[i].A = 255; 31 | } 32 | 33 | if (colors[0] > colors[1]) 34 | { 35 | color[2].R = (byte)((2 * color[0].R + color[1].R) / 3); 36 | color[2].G = (byte)((2 * color[0].G + color[1].G) / 3); 37 | color[2].B = (byte)((2 * color[0].B + color[1].B) / 3); 38 | color[2].A = 255; 39 | 40 | color[3].R = (byte)((color[0].R + 2 * color[1].R) / 3); 41 | color[3].G = (byte)((color[0].G + 2 * color[1].G) / 3); 42 | color[3].B = (byte)((color[0].B + 2 * color[1].B) / 3); 43 | color[3].A = 255; 44 | } 45 | else 46 | { 47 | color[2].R = (byte)((color[0].R + color[1].R) / 2); 48 | color[2].G = (byte)((color[0].G + color[1].G) / 2); 49 | color[2].B = (byte)((color[0].B + color[1].B) / 2); 50 | color[2].A = 255; 51 | 52 | color[3].R = 0; 53 | color[3].G = 0; 54 | color[3].B = 0; 55 | color[3].A = 0; 56 | } 57 | 58 | for (int yOffset = 0; yOffset < 4; yOffset++) 59 | { 60 | var rowVal = src[srcOffset++]; 61 | for (int xOffset = 0; xOffset < 4; xOffset++) 62 | { 63 | var pixel = color[(rowVal >> (xOffset << 1)) & 0x03]; 64 | dest[x + xOffset, y + yOffset] = pixel; 65 | } 66 | } 67 | 68 | return srcOffset; 69 | } 70 | 71 | private static int DXT3toARGB(byte[] src, int srcOffset, Image dest, int x, int y) 72 | { 73 | int alphaOffset = srcOffset; 74 | 75 | srcOffset += 8; 76 | 77 | ushort[] colors = new ushort[2]; 78 | Rgba32[] color = new Rgba32[4]; 79 | 80 | for (var i = 0; i < 2; i++) 81 | { 82 | colors[i] = src[srcOffset++]; 83 | colors[i] |= (ushort)(src[srcOffset++] << 8); 84 | 85 | color[i].R = (byte)((colors[i] & 0xF800) >> 11); 86 | color[i].G = (byte)((colors[i] & 0x7E0) >> 5); 87 | color[i].B = (byte)(colors[i] & 0x1f); 88 | color[i].R = (byte)(color[i].R << 3 | color[i].R >> 2); 89 | color[i].G = (byte)(color[i].G << 2 | color[i].G >> 3); 90 | color[i].B = (byte)(color[i].B << 3 | color[i].B >> 2); 91 | color[i].A = 255; 92 | } 93 | 94 | color[2].R = (byte)((2 * color[0].R + color[1].R) / 3); 95 | color[2].G = (byte)((2 * color[0].G + color[1].G) / 3); 96 | color[2].B = (byte)((2 * color[0].B + color[1].B) / 3); 97 | color[2].A = 255; 98 | 99 | color[3].R = (byte)((color[0].R + 2 * color[1].R) / 3); 100 | color[3].G = (byte)((color[0].G + 2 * color[1].G) / 3); 101 | color[3].B = (byte)((color[0].B + 2 * color[1].B) / 3); 102 | color[3].A = 255; 103 | 104 | for (int yOffset = 0; yOffset < 4; yOffset++) 105 | { 106 | var rowVal = src[srcOffset++]; 107 | 108 | ushort rowAlpha = src[alphaOffset++]; 109 | rowAlpha |= (ushort)(src[alphaOffset++] << 8); 110 | 111 | for (int xOffset = 0; xOffset < 4; xOffset++) 112 | { 113 | byte currentAlpha = (byte)((rowAlpha >> (xOffset * 4)) & 0x0f); 114 | currentAlpha |= (byte)(currentAlpha << 4); 115 | 116 | var pixel = color[(rowVal >> (xOffset << 1)) & 0x03]; 117 | pixel.A = currentAlpha; 118 | 119 | dest[x + xOffset, y + yOffset] = pixel; 120 | } 121 | } 122 | 123 | return srcOffset; 124 | } 125 | 126 | private static void Unswizzle(byte[] src, uint depth, uint width, uint height, ref byte[] dest) 127 | { 128 | for (uint y = 0; y < height; y++) 129 | { 130 | uint sy = 0; 131 | if (y < width) 132 | { 133 | for (int bit = 0; bit < 16; bit++) 134 | { 135 | sy |= (y >> bit & 1) << 2 * bit; 136 | } 137 | sy <<= 1; // y counts twice 138 | } 139 | else 140 | { 141 | uint y_mask = y % width; 142 | for (int bit = 0; bit < 16; bit++) 143 | { 144 | sy |= (y_mask >> bit & 1) << 2 * bit; 145 | } 146 | sy <<= 1; // y counts twice 147 | sy += y / width * width * width; 148 | } 149 | uint d = y * width * depth; 150 | for (uint x = 0; x < width; x++) 151 | { 152 | uint sx = 0; 153 | if (x < height * 2) 154 | { 155 | for (int bit = 0; bit < 16; bit++) 156 | { 157 | sx |= (x >> bit & 1) << 2 * bit; 158 | } 159 | } 160 | else 161 | { 162 | uint x_mask = x % (2 * height); 163 | for (int bit = 0; bit < 16; bit++) 164 | { 165 | sx |= (x_mask >> bit & 1) << 2 * bit; 166 | } 167 | sx += x / (2 * height) * 2 * height * height; 168 | } 169 | uint s = (sx + sy) * depth; 170 | for (uint i = 0; i < depth; ++i) 171 | { 172 | dest[d] = src[s + i]; 173 | d = d + 1; 174 | } 175 | } 176 | } 177 | } 178 | 179 | private static void ConvertDXT1(byte[] src, Image dest) 180 | { 181 | var srcOffset = 0; 182 | var width = dest.Width; 183 | var height = dest.Height; 184 | for (var y = 0; y < height; y += 4) 185 | { 186 | for (var x = 0; x < width; x += 4) 187 | { 188 | srcOffset = DXT1toARGB(src, srcOffset, dest, x, y); 189 | } 190 | } 191 | } 192 | 193 | private static void ConvertDXT3(byte[] src, Image dest) 194 | { 195 | var srcOffset = 0; 196 | var width = dest.Width; 197 | var height = dest.Height; 198 | for (var y = 0; y < height; y += 4) 199 | { 200 | for (var x = 0; x < width; x += 4) 201 | { 202 | srcOffset = DXT3toARGB(src, srcOffset, dest, x, y); 203 | } 204 | } 205 | } 206 | 207 | private static void ConvertARGB(byte[] src, Image dest) 208 | { 209 | var buffer = new byte[dest.Width * dest.Height * 4]; 210 | Unswizzle(src, 4, (uint)dest.Width, (uint)dest.Height, ref buffer); 211 | 212 | var srcOffset = 0; 213 | var width = dest.Width; 214 | var height = dest.Height; 215 | for (int y = 0; y < height; y++) 216 | { 217 | for (int x = 0; x < width; x++) 218 | { 219 | var blue = buffer[srcOffset++]; 220 | var green = buffer[srcOffset++]; 221 | var red = buffer[srcOffset++]; 222 | var alpha = buffer[srcOffset++]; 223 | dest[x, y] = new Rgba32(red, green, blue, alpha); 224 | } 225 | } 226 | } 227 | 228 | private static void ConvertRGB(byte[] src, Image dest) 229 | { 230 | var buffer = new byte[dest.Width * dest.Height * 4]; 231 | Unswizzle(src, 4, (uint)dest.Width, (uint)dest.Height, ref buffer); 232 | 233 | var srcOffset = 0; 234 | var width = dest.Width; 235 | var height = dest.Height; 236 | for (int y = 0; y < height; y++) 237 | { 238 | for (int x = 0; x < width; x++) 239 | { 240 | var blue = buffer[srcOffset++]; 241 | var green = buffer[srcOffset++]; 242 | var red = buffer[srcOffset++]; 243 | srcOffset++; 244 | dest[x, y] = new Rgba32(red, green, blue, 255); 245 | } 246 | } 247 | } 248 | 249 | private static void ConvertRGBA(byte[] src, Image dest) 250 | { 251 | var buffer = new byte[dest.Width * dest.Height * 4]; 252 | Unswizzle(src, 4, (uint)dest.Width, (uint)dest.Height, ref buffer); 253 | 254 | var srcOffset = 0; 255 | var width = dest.Width; 256 | var height = dest.Height; 257 | for (int y = 0; y < height; y++) 258 | { 259 | for (int x = 0; x < width; x++) 260 | { 261 | var alpha = buffer[srcOffset++]; 262 | var blue = buffer[srcOffset++]; 263 | var green = buffer[srcOffset++]; 264 | var red = buffer[srcOffset++]; 265 | dest[x, y] = new Rgba32(red, green, blue, alpha); 266 | } 267 | } 268 | } 269 | 270 | private static void ConvertA4R4G4B4(byte[] src, Image dest) 271 | { 272 | var buffer = new byte[dest.Width * dest.Height * 2]; 273 | Unswizzle(src, 2, (uint)dest.Width, (uint)dest.Height, ref buffer); 274 | 275 | var srcOffset = 0; 276 | var width = dest.Width; 277 | var height = dest.Height; 278 | for (int y = 0; y < height; y++) 279 | { 280 | for (int x = 0; x < width; x++) 281 | { 282 | var blue = (byte)((buffer[srcOffset + 0] & 0x0f) << 4); 283 | var green = (byte)(buffer[srcOffset++] & 0xf0); 284 | var red = (byte)((buffer[srcOffset] & 0x0f) << 4); 285 | var alpha = (byte)(buffer[srcOffset++] & 0xf0); 286 | dest[x, y] = new Rgba32(red, green, blue, alpha); 287 | } 288 | } 289 | } 290 | 291 | 292 | private static void ConvertR5G6B5(byte[] src, Image dest) 293 | { 294 | var buffer = new byte[dest.Width * dest.Height * 2]; 295 | Unswizzle(src, 2, (uint)dest.Width, (uint)dest.Height, ref buffer); 296 | 297 | var srcOffset = 0; 298 | var width = dest.Width; 299 | var height = dest.Height; 300 | for (int y = 0; y < height; y++) 301 | { 302 | for (int x = 0; x < width; x++) 303 | { 304 | var blue = (byte)((buffer[srcOffset] & 0x1f) << 3); 305 | var green = (byte)((buffer[srcOffset++] & 0xe0) >> 3); 306 | green |= (byte)((buffer[srcOffset] & 0x07) << 5); 307 | var red = (byte)(buffer[srcOffset++] & 0xf8); 308 | dest[x, y] = new Rgba32(red, green, blue, 255); 309 | } 310 | } 311 | } 312 | 313 | public static bool ConvertDdsToPng(byte[]? input, out byte[]? output) 314 | { 315 | output = null; 316 | 317 | if (input == null) 318 | { 319 | return false; 320 | } 321 | 322 | using var inputStream = new MemoryStream(input); 323 | using var reader = new BinaryReader(inputStream); 324 | 325 | var magic = reader.ReadUInt32(); 326 | if (magic != 0x20534444) 327 | { 328 | return false; 329 | } 330 | 331 | var header = StructUtility.ByteToType(reader); 332 | 333 | 334 | if (header.PixelFormat.FourCC == 0x31545844) 335 | { 336 | try 337 | { 338 | using var image = new Image((int)header.Width, (int)header.Height); 339 | var imageData = reader.ReadBytes((int)((header.Width * header.Height) << 1)); 340 | ConvertDXT1(imageData, image); 341 | 342 | using var outputStream = new MemoryStream(); 343 | image.SaveAsPng(outputStream); 344 | output = outputStream.ToArray(); 345 | return true; 346 | } 347 | catch (Exception) 348 | { 349 | return false; 350 | } 351 | } 352 | else if (header.PixelFormat.FourCC == 0x33545844) 353 | { 354 | try 355 | { 356 | using var image = new Image((int)header.Width, (int)header.Height); 357 | var imageData = reader.ReadBytes((int)(header.Width * header.Height)); 358 | ConvertDXT3(imageData, image); 359 | 360 | using var outputStream = new MemoryStream(); 361 | image.SaveAsPng(outputStream); 362 | output = outputStream.ToArray(); 363 | return true; 364 | 365 | } 366 | catch (Exception) 367 | { 368 | return false; 369 | } 370 | } 371 | return false; 372 | } 373 | 374 | 375 | public static bool ConvertXprToJpeg(byte[]? input, out byte[]? output) 376 | { 377 | output = null; 378 | 379 | if (input == null) 380 | { 381 | return false; 382 | } 383 | 384 | using var inputStream = new MemoryStream(input); 385 | using var reader = new BinaryReader(inputStream); 386 | 387 | var magic = reader.ReadUInt32(); 388 | 389 | if (magic == 0x20534444) 390 | { 391 | return ConvertDdsToPng(input, out output); 392 | } 393 | 394 | if (magic != 0x30525058) 395 | { 396 | try 397 | { 398 | inputStream.Position = 0; 399 | using var imagex = Image.Load(input); 400 | using var outputStream2 = new MemoryStream(); 401 | imagex.SaveAsPng(outputStream2); 402 | output = outputStream2.ToArray(); 403 | return true; 404 | } 405 | catch 406 | { 407 | } 408 | return false; 409 | } 410 | 411 | var totalSize = reader.ReadUInt32(); 412 | if (totalSize > input.Length) 413 | { 414 | return false; 415 | } 416 | 417 | var headerSize = reader.ReadUInt32(); 418 | var flags = reader.ReadUInt32(); 419 | var count = flags & 0xffff; 420 | if (count != 1) 421 | { 422 | return false; 423 | } 424 | 425 | var type = flags >> 16 & 0x7; 426 | if (type != 4) 427 | { 428 | return false; 429 | } 430 | 431 | var unused1 = reader.ReadUInt32(); 432 | if (unused1 != 0) 433 | { 434 | return false; 435 | } 436 | 437 | var unused2 = reader.ReadUInt32(); 438 | if (unused2 != 0) 439 | { 440 | return false; 441 | } 442 | 443 | var format = reader.ReadUInt32(); 444 | var mipLevels = (int)(format >> 24 & 0xff); 445 | var dimension = 1 << mipLevels; 446 | 447 | using var image = new Image(dimension, dimension); 448 | 449 | var unused3 = reader.ReadInt32(); 450 | if (unused3 != 0) 451 | { 452 | return false; 453 | } 454 | 455 | if (headerSize != 32) 456 | { 457 | var end = reader.ReadUInt32(); 458 | if (end != 0xffffffff) 459 | { 460 | return false; 461 | } 462 | } 463 | 464 | var paddingSize = headerSize - inputStream.Position; 465 | if (paddingSize > 0) 466 | { 467 | _ = reader.ReadBytes((int)paddingSize); 468 | } 469 | 470 | var imageSize = totalSize - headerSize; 471 | var imageData = reader.ReadBytes((int)imageSize); 472 | 473 | var dxt = format >> 8 & 0xff; 474 | if (dxt == 0x0c) 475 | { 476 | ConvertDXT1(imageData, image); 477 | } 478 | else if (dxt == 0x0e) 479 | { 480 | ConvertDXT3(imageData, image); 481 | } 482 | else if (dxt == 0x04) 483 | { 484 | ConvertA4R4G4B4(imageData, image); 485 | } 486 | else if (dxt == 0x05) 487 | { 488 | ConvertR5G6B5(imageData, image); 489 | } 490 | else if (dxt == 0x06) 491 | { 492 | ConvertARGB(imageData, image); 493 | } 494 | else if (dxt == 0x07) 495 | { 496 | ConvertRGB(imageData, image); 497 | } 498 | else if (dxt == 0x3c) 499 | { 500 | ConvertRGBA(imageData, image); 501 | } 502 | else 503 | { 504 | return false; 505 | } 506 | 507 | using var outputStream = new MemoryStream(); 508 | image.SaveAsJpeg(outputStream); 509 | output = outputStream.ToArray(); 510 | 511 | var result = inputStream.Position == totalSize; 512 | return result; 513 | } 514 | 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /XboxToolkit/Interface/ContainerReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using XboxToolkit.Internal.Models; 6 | using XboxToolkit.Internal; 7 | 8 | namespace XboxToolkit.Interface 9 | { 10 | public abstract class ContainerReader : IContainerReader, IDisposable 11 | { 12 | public abstract SectorDecoder GetDecoder(); 13 | 14 | public abstract bool TryMount(); 15 | 16 | public abstract void Dismount(); 17 | 18 | public abstract int GetMountCount(); 19 | 20 | public bool TryExtractFiles(string destFilePath, Action? progress = null) 21 | { 22 | try 23 | { 24 | Directory.CreateDirectory(destFilePath); 25 | 26 | var decoder = GetDecoder(); 27 | var xgdInfo = decoder.GetXgdInfo(); 28 | 29 | var rootSectors = (xgdInfo.RootDirSize + Constants.XGD_SECTOR_SIZE - 1) / Constants.XGD_SECTOR_SIZE; 30 | var rootData = new byte[xgdInfo.RootDirSize]; 31 | for (var i = 0; i < rootSectors; i++) 32 | { 33 | var currentRootSector = xgdInfo.BaseSector + xgdInfo.RootDirSector + (uint)i; 34 | if (decoder.TryReadSector(currentRootSector, out var sectorData) == false) 35 | { 36 | return false; 37 | } 38 | var offset = i * Constants.XGD_SECTOR_SIZE; 39 | var length = Math.Min(Constants.XGD_SECTOR_SIZE, xgdInfo.RootDirSize - offset); 40 | Array.Copy(sectorData, 0, rootData, offset, length); 41 | } 42 | 43 | var treeNodes = new List 44 | { 45 | new TreeNodeInfo 46 | { 47 | DirectoryData = rootData, 48 | Offset = 0, 49 | Path = string.Empty 50 | } 51 | }; 52 | 53 | while (treeNodes.Count > 0) 54 | { 55 | // Use stack (LIFO) for preorder traversal - pop from end 56 | var currentTreeNode = treeNodes[treeNodes.Count - 1]; 57 | treeNodes.RemoveAt(treeNodes.Count - 1); 58 | 59 | using (var directoryDataStream = new MemoryStream(currentTreeNode.DirectoryData)) 60 | using (var directoryDataDataReader = new BinaryReader(directoryDataStream)) 61 | { 62 | 63 | if (currentTreeNode.Offset * 4 >= directoryDataStream.Length) 64 | { 65 | continue; 66 | } 67 | 68 | var entryOffset = currentTreeNode.Offset * 4; 69 | directoryDataStream.Position = entryOffset; 70 | 71 | // Read first 14 bytes (DirectoryEntryDiskNode structure) 72 | var headerBuffer = new byte[14]; 73 | if (directoryDataStream.Read(headerBuffer, 0, 14) != 14) 74 | { 75 | continue; 76 | } 77 | 78 | // Check if entry is empty (all 0xFF or all 0x00) - matches xdvdfs deserialize check 79 | bool allFF = true; 80 | bool allZero = true; 81 | for (int i = 0; i < 14; i++) 82 | { 83 | if (headerBuffer[i] != 0xFF) allFF = false; 84 | if (headerBuffer[i] != 0x00) allZero = false; 85 | } 86 | if (allFF || allZero) 87 | { 88 | continue; 89 | } 90 | 91 | // Parse the header from buffer (matches xdvdfs deserialize) 92 | // xdvdfs uses little-endian deserialization 93 | var left = (ushort)(headerBuffer[0] | (headerBuffer[1] << 8)); 94 | var right = (ushort)(headerBuffer[2] | (headerBuffer[3] << 8)); 95 | var sector = (uint)(headerBuffer[4] | (headerBuffer[5] << 8) | (headerBuffer[6] << 16) | (headerBuffer[7] << 24)); 96 | var size = (uint)(headerBuffer[8] | (headerBuffer[9] << 8) | (headerBuffer[10] << 16) | (headerBuffer[11] << 24)); 97 | var attribute = headerBuffer[12]; 98 | var nameLength = headerBuffer[13]; 99 | 100 | // Validate nameLength is reasonable 101 | if (nameLength == 0 || nameLength > 255) 102 | { 103 | continue; 104 | } 105 | 106 | // Validate we have enough bytes to read the filename 107 | var filenameOffset = entryOffset + 14; // 0xe in xdvdfs 108 | if (filenameOffset + nameLength > directoryDataStream.Length) 109 | { 110 | continue; 111 | } 112 | 113 | // Read filename at offset + 0xe (matches xdvdfs read_from_disk) 114 | directoryDataStream.Position = filenameOffset; 115 | var filenameBytes = directoryDataDataReader.ReadBytes(nameLength); 116 | var filename = Encoding.ASCII.GetString(filenameBytes); 117 | 118 | // Process current entry first (preorder traversal) 119 | var path = Path.Combine(destFilePath, currentTreeNode.Path, filename); 120 | var relativePath = string.IsNullOrEmpty(currentTreeNode.Path) ? filename : Path.Combine(currentTreeNode.Path, filename).Replace('\\', '/'); 121 | 122 | // Report progress with current filename 123 | progress?.Invoke(relativePath); 124 | 125 | if ((attribute & 0x10) != 0) 126 | { 127 | Directory.CreateDirectory(path); 128 | } 129 | else 130 | { 131 | if (size > 0) 132 | { 133 | using (var fileStream = File.OpenWrite(path)) 134 | { 135 | var readSector = sector + xgdInfo.BaseSector; 136 | var processed = 0U; 137 | while (processed < size) 138 | { 139 | if (decoder.TryReadSector(readSector, out var buffer) == false) 140 | { 141 | return false; 142 | } 143 | var bytesToSave = Math.Min(size - processed, Constants.XGD_SECTOR_SIZE); 144 | fileStream.Write(buffer, 0, (int)bytesToSave); 145 | readSector++; 146 | processed += bytesToSave; 147 | } 148 | } 149 | } 150 | else 151 | { 152 | File.Create(path).Close(); 153 | } 154 | } 155 | 156 | // Push right first, then left (so left is processed first when popping) 157 | // Validate right offset is within bounds 158 | if (right != 0 && right != 0xFFFF) 159 | { 160 | var rightOffsetBytes = right * 4; 161 | if (rightOffsetBytes < directoryDataStream.Length) 162 | { 163 | treeNodes.Add(new TreeNodeInfo 164 | { 165 | DirectoryData = currentTreeNode.DirectoryData, 166 | Offset = right, 167 | Path = currentTreeNode.Path 168 | }); 169 | } 170 | } 171 | 172 | // Validate left offset is within bounds 173 | if (left != 0 && left != 0xFFFF) 174 | { 175 | var leftOffsetBytes = left * 4; 176 | if (leftOffsetBytes < directoryDataStream.Length) 177 | { 178 | treeNodes.Add(new TreeNodeInfo 179 | { 180 | DirectoryData = currentTreeNode.DirectoryData, 181 | Offset = left, 182 | Path = currentTreeNode.Path 183 | }); 184 | } 185 | } 186 | 187 | // If directory, load its data and add to stack 188 | if ((attribute & 0x10) != 0) 189 | { 190 | if (size > 0) 191 | { 192 | var directorySectors = (size + Constants.XGD_SECTOR_SIZE - 1) / Constants.XGD_SECTOR_SIZE; 193 | var directoryData = new byte[size]; 194 | for (var i = 0; i < directorySectors; i++) 195 | { 196 | var currentDirectorySector = xgdInfo.BaseSector + sector + (uint)i; 197 | if (decoder.TryReadSector(currentDirectorySector, out var sectorData) == false) 198 | { 199 | return false; 200 | } 201 | var offset = i * Constants.XGD_SECTOR_SIZE; 202 | var length = Math.Min(Constants.XGD_SECTOR_SIZE, size - offset); 203 | Array.Copy(sectorData, 0, directoryData, offset, length); 204 | } 205 | 206 | treeNodes.Add(new TreeNodeInfo 207 | { 208 | DirectoryData = directoryData, 209 | Offset = 0, 210 | Path = Path.Combine(currentTreeNode.Path, filename) 211 | }); 212 | } 213 | } 214 | } 215 | } 216 | return true; 217 | } 218 | catch (Exception ex) 219 | { 220 | System.Diagnostics.Debug.Print(ex.ToString()); 221 | return false; 222 | } 223 | } 224 | 225 | public bool TryGetDataSectors(out HashSet dataSectors) 226 | { 227 | dataSectors = new HashSet(); 228 | 229 | try 230 | { 231 | var decoder = GetDecoder(); 232 | var xgdInfo = decoder.GetXgdInfo(); 233 | 234 | dataSectors.Add(xgdInfo.BaseSector + Constants.XGD_ISO_BASE_SECTOR); 235 | dataSectors.Add(xgdInfo.BaseSector + Constants.XGD_ISO_BASE_SECTOR + 1); 236 | 237 | var rootSectors = (xgdInfo.RootDirSize + Constants.XGD_SECTOR_SIZE - 1) / Constants.XGD_SECTOR_SIZE; 238 | var rootData = new byte[xgdInfo.RootDirSize]; 239 | for (var i = 0; i < rootSectors; i++) 240 | { 241 | var currentRootSector = xgdInfo.BaseSector + xgdInfo.RootDirSector + (uint)i; 242 | dataSectors.Add(currentRootSector); 243 | if (decoder.TryReadSector(currentRootSector, out var sectorData) == false) 244 | { 245 | return false; 246 | } 247 | var offset = i * Constants.XGD_SECTOR_SIZE; 248 | var length = Math.Min(Constants.XGD_SECTOR_SIZE, xgdInfo.RootDirSize - offset); 249 | Array.Copy(sectorData, 0, rootData, offset, length); 250 | } 251 | 252 | var treeNodes = new List 253 | { 254 | new TreeNodeInfo 255 | { 256 | DirectoryData = rootData, 257 | Offset = 0, 258 | Path = string.Empty 259 | } 260 | }; 261 | 262 | while (treeNodes.Count > 0) 263 | { 264 | // Use stack (LIFO) for preorder traversal - pop from end 265 | var currentTreeNode = treeNodes[treeNodes.Count - 1]; 266 | treeNodes.RemoveAt(treeNodes.Count - 1); 267 | 268 | using (var directoryDataStream = new MemoryStream(currentTreeNode.DirectoryData)) 269 | using (var directoryDataDataReader = new BinaryReader(directoryDataStream)) 270 | { 271 | 272 | if (currentTreeNode.Offset * 4 >= directoryDataStream.Length) 273 | { 274 | continue; 275 | } 276 | 277 | var entryOffset = currentTreeNode.Offset * 4; 278 | directoryDataStream.Position = entryOffset; 279 | 280 | // Read first 14 bytes (DirectoryEntryDiskNode structure) 281 | var headerBuffer = new byte[14]; 282 | if (directoryDataStream.Read(headerBuffer, 0, 14) != 14) 283 | { 284 | continue; 285 | } 286 | 287 | // Check if entry is empty (all 0xFF or all 0x00) - matches xdvdfs deserialize check 288 | bool allFF = true; 289 | bool allZero = true; 290 | for (int i = 0; i < 14; i++) 291 | { 292 | if (headerBuffer[i] != 0xFF) allFF = false; 293 | if (headerBuffer[i] != 0x00) allZero = false; 294 | } 295 | if (allFF || allZero) 296 | { 297 | continue; 298 | } 299 | 300 | // Parse the header from buffer (matches xdvdfs deserialize) 301 | // xdvdfs uses little-endian deserialization 302 | var left = (ushort)(headerBuffer[0] | (headerBuffer[1] << 8)); 303 | var right = (ushort)(headerBuffer[2] | (headerBuffer[3] << 8)); 304 | var sector = (uint)(headerBuffer[4] | (headerBuffer[5] << 8) | (headerBuffer[6] << 16) | (headerBuffer[7] << 24)); 305 | var size = (uint)(headerBuffer[8] | (headerBuffer[9] << 8) | (headerBuffer[10] << 16) | (headerBuffer[11] << 24)); 306 | var attribute = headerBuffer[12]; 307 | var nameLength = headerBuffer[13]; 308 | 309 | // Validate nameLength is reasonable 310 | if (nameLength == 0 || nameLength > 255) 311 | { 312 | continue; 313 | } 314 | 315 | // Validate we have enough bytes to read the filename 316 | var filenameOffset = entryOffset + 14; // 0xe in xdvdfs 317 | if (filenameOffset + nameLength > directoryDataStream.Length) 318 | { 319 | continue; 320 | } 321 | 322 | // Read filename at offset + 0xe (matches xdvdfs read_from_disk) 323 | directoryDataStream.Position = filenameOffset; 324 | var filenameBytes = directoryDataDataReader.ReadBytes(nameLength); 325 | var filename = Encoding.ASCII.GetString(filenameBytes); 326 | 327 | // Process current entry first (preorder traversal) 328 | if (size > 0) 329 | { 330 | if ((attribute & 0x10) != 0) 331 | { 332 | var directorySectors = (size + Constants.XGD_SECTOR_SIZE - 1) / Constants.XGD_SECTOR_SIZE; 333 | var directoryData = new byte[size]; 334 | for (var i = 0; i < directorySectors; i++) 335 | { 336 | var currentDirectorySector = xgdInfo.BaseSector + sector + (uint)i; 337 | dataSectors.Add(currentDirectorySector); 338 | if (decoder.TryReadSector(currentDirectorySector, out var sectorData) == false) 339 | { 340 | return false; 341 | } 342 | var offset = i * Constants.XGD_SECTOR_SIZE; 343 | var length = Math.Min(Constants.XGD_SECTOR_SIZE, size - offset); 344 | Array.Copy(sectorData, 0, directoryData, offset, length); 345 | } 346 | 347 | treeNodes.Add(new TreeNodeInfo 348 | { 349 | DirectoryData = directoryData, 350 | Offset = 0, 351 | Path = Path.Combine(currentTreeNode.Path, filename) 352 | }); 353 | } 354 | else 355 | { 356 | var fileSectors = Helpers.RoundToMultiple(size, Constants.XGD_SECTOR_SIZE) / Constants.XGD_SECTOR_SIZE; 357 | for (var i = 0; i < fileSectors; i++) 358 | { 359 | var currentFileSector = xgdInfo.BaseSector + sector + (uint)i; 360 | dataSectors.Add(currentFileSector); 361 | } 362 | } 363 | } 364 | 365 | // Push right first, then left (so left is processed first when popping) 366 | // Validate right offset is within bounds 367 | if (right != 0 && right != 0xFFFF) 368 | { 369 | var rightOffsetBytes = right * 4; 370 | if (rightOffsetBytes < directoryDataStream.Length) 371 | { 372 | treeNodes.Add(new TreeNodeInfo 373 | { 374 | DirectoryData = currentTreeNode.DirectoryData, 375 | Offset = right, 376 | Path = currentTreeNode.Path 377 | }); 378 | } 379 | } 380 | 381 | // Validate left offset is within bounds 382 | if (left != 0 && left != 0xFFFF) 383 | { 384 | var leftOffsetBytes = left * 4; 385 | if (leftOffsetBytes < directoryDataStream.Length) 386 | { 387 | treeNodes.Add(new TreeNodeInfo 388 | { 389 | DirectoryData = currentTreeNode.DirectoryData, 390 | Offset = left, 391 | Path = currentTreeNode.Path 392 | }); 393 | } 394 | } 395 | } 396 | } 397 | return true; 398 | } 399 | catch (Exception ex) 400 | { 401 | System.Diagnostics.Debug.Print(ex.ToString()); 402 | return false; 403 | } 404 | } 405 | 406 | public bool TryGetDefault(out byte[] defaultData, out ContainerType containerType) 407 | { 408 | defaultData = Array.Empty(); 409 | containerType = ContainerType.Unknown; 410 | 411 | try 412 | { 413 | var decoder = GetDecoder(); 414 | var xgdInfo = decoder.GetXgdInfo(); 415 | 416 | var rootSectors = (xgdInfo.RootDirSize + Constants.XGD_SECTOR_SIZE - 1) / Constants.XGD_SECTOR_SIZE; 417 | var rootData = new byte[xgdInfo.RootDirSize]; 418 | for (var i = 0; i < rootSectors; i++) 419 | { 420 | if (decoder.TryReadSector(xgdInfo.BaseSector + xgdInfo.RootDirSector + (uint)i, out var sectorData) == false) 421 | { 422 | return false; 423 | } 424 | var offset = i * Constants.XGD_SECTOR_SIZE; 425 | var length = Math.Min(Constants.XGD_SECTOR_SIZE, xgdInfo.RootDirSize - offset); 426 | Array.Copy(sectorData, 0, rootData, offset, length); 427 | } 428 | 429 | var treeNodes = new List 430 | { 431 | new TreeNodeInfo 432 | { 433 | DirectoryData = rootData, 434 | Offset = 0, 435 | Path = string.Empty 436 | } 437 | }; 438 | 439 | while (treeNodes.Count > 0) 440 | { 441 | // Use stack (LIFO) for preorder traversal - pop from end 442 | var currentTreeNode = treeNodes[treeNodes.Count - 1]; 443 | treeNodes.RemoveAt(treeNodes.Count - 1); 444 | 445 | using (var directoryDataStream = new MemoryStream(currentTreeNode.DirectoryData)) 446 | using (var directoryDataDataReader = new BinaryReader(directoryDataStream)) 447 | { 448 | 449 | if (currentTreeNode.Offset * 4 >= directoryDataStream.Length) 450 | { 451 | continue; 452 | } 453 | 454 | var entryOffset = currentTreeNode.Offset * 4; 455 | directoryDataStream.Position = entryOffset; 456 | 457 | // Read first 14 bytes (DirectoryEntryDiskNode structure) 458 | var headerBuffer = new byte[14]; 459 | if (directoryDataStream.Read(headerBuffer, 0, 14) != 14) 460 | { 461 | continue; 462 | } 463 | 464 | // Check if entry is empty (all 0xFF or all 0x00) - matches xdvdfs deserialize check 465 | bool allFF = true; 466 | bool allZero = true; 467 | for (int i = 0; i < 14; i++) 468 | { 469 | if (headerBuffer[i] != 0xFF) allFF = false; 470 | if (headerBuffer[i] != 0x00) allZero = false; 471 | } 472 | if (allFF || allZero) 473 | { 474 | continue; 475 | } 476 | 477 | // Parse the header from buffer (matches xdvdfs deserialize) 478 | // xdvdfs uses little-endian deserialization 479 | var left = (ushort)(headerBuffer[0] | (headerBuffer[1] << 8)); 480 | var right = (ushort)(headerBuffer[2] | (headerBuffer[3] << 8)); 481 | var sector = (uint)(headerBuffer[4] | (headerBuffer[5] << 8) | (headerBuffer[6] << 16) | (headerBuffer[7] << 24)); 482 | var size = (uint)(headerBuffer[8] | (headerBuffer[9] << 8) | (headerBuffer[10] << 16) | (headerBuffer[11] << 24)); 483 | var attribute = headerBuffer[12]; 484 | var nameLength = headerBuffer[13]; 485 | 486 | // Validate nameLength is reasonable 487 | if (nameLength == 0 || nameLength > 255) 488 | { 489 | continue; 490 | } 491 | 492 | // Validate we have enough bytes to read the filename 493 | var filenameOffset = entryOffset + 14; // 0xe in xdvdfs 494 | if (filenameOffset + nameLength > directoryDataStream.Length) 495 | { 496 | continue; 497 | } 498 | 499 | // Read filename at offset + 0xe (matches xdvdfs read_from_disk) 500 | directoryDataStream.Position = filenameOffset; 501 | var filenameBytes = directoryDataDataReader.ReadBytes(nameLength); 502 | var filename = Encoding.ASCII.GetString(filenameBytes); 503 | 504 | var isXbe = filename.Equals(Constants.XBE_FILE_NAME, StringComparison.CurrentCultureIgnoreCase); 505 | var isXex = filename.Equals(Constants.XEX_FILE_NAME, StringComparison.CurrentCultureIgnoreCase); 506 | if (isXbe || isXex) 507 | { 508 | containerType = isXbe ? ContainerType.XboxOriginal : ContainerType.Xbox360; 509 | 510 | var readSector = sector + xgdInfo.BaseSector; 511 | var result = new byte[size]; 512 | var processed = 0U; 513 | if (size > 0) 514 | { 515 | while (processed < size) 516 | { 517 | if (decoder.TryReadSector(readSector, out var buffer) == false) 518 | { 519 | return false; 520 | } 521 | var bytesToCopy = Math.Min(size - processed, Constants.XGD_SECTOR_SIZE); 522 | Array.Copy(buffer, 0, result, processed, bytesToCopy); 523 | readSector++; 524 | processed += bytesToCopy; 525 | } 526 | } 527 | defaultData = result; 528 | return true; 529 | } 530 | 531 | // Push right first, then left (so left is processed first when popping) 532 | // Validate right offset is within bounds 533 | if (right != 0 && right != 0xFFFF) 534 | { 535 | var rightOffsetBytes = right * 4; 536 | if (rightOffsetBytes < directoryDataStream.Length) 537 | { 538 | treeNodes.Add(new TreeNodeInfo 539 | { 540 | DirectoryData = currentTreeNode.DirectoryData, 541 | Offset = right, 542 | Path = currentTreeNode.Path 543 | }); 544 | } 545 | } 546 | 547 | // Validate left offset is within bounds 548 | if (left != 0 && left != 0xFFFF) 549 | { 550 | var leftOffsetBytes = left * 4; 551 | if (leftOffsetBytes < directoryDataStream.Length) 552 | { 553 | treeNodes.Add(new TreeNodeInfo 554 | { 555 | DirectoryData = currentTreeNode.DirectoryData, 556 | Offset = left, 557 | Path = currentTreeNode.Path 558 | }); 559 | } 560 | } 561 | } 562 | } 563 | return false; 564 | } 565 | catch (Exception ex) 566 | { 567 | System.Diagnostics.Debug.Print(ex.ToString()); 568 | return false; 569 | } 570 | } 571 | 572 | public virtual void Dispose() 573 | { 574 | throw new NotImplementedException(); 575 | } 576 | } 577 | } 578 | -------------------------------------------------------------------------------- /XboxToolkit/XexUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Xml; 6 | using System.Xml.Linq; 7 | using System.Xml.XPath; 8 | using XboxToolkit.Internal; 9 | using XboxToolkit.Internal.Models; 10 | using XboxToolkit.Models; 11 | 12 | namespace XboxToolkit 13 | { 14 | public static partial class XexUtility 15 | { 16 | private static readonly byte[] XexDevkitKey = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; 17 | private static readonly byte[] XexRetailKey = { 0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3, 0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91 }; 18 | 19 | private static readonly uint XexExecutionId = 0x400; 20 | private static readonly uint XexHeaderSectionTableId = 0x2; 21 | private static readonly uint XexFileDataDescriptorId = 0x3; 22 | private static readonly uint XexDataFlagEncrypted = 0x1; 23 | private static readonly uint XexDataFormatRaw = 0x1; 24 | private static readonly uint XexDataFormatCompressed = 0x2; 25 | private static readonly uint XexDataFormatDeltaCompressed = 0x3; 26 | 27 | private static readonly uint XSRC = 0x58535243; 28 | 29 | private static bool SearchField(BinaryReader binaryReader, XexHeader header, uint searchId, out T result) where T : struct 30 | { 31 | result = default; 32 | 33 | binaryReader.BaseStream.Position = Helpers.SizeOf(); 34 | var headerDirectoryEntryCount = Helpers.ConvertEndian(header.HeaderDirectoryEntryCount); 35 | 36 | for (var i = 0; i < headerDirectoryEntryCount; i++) 37 | { 38 | var value = Helpers.ConvertEndian(binaryReader.ReadUInt32()); 39 | var offset = Helpers.ConvertEndian(binaryReader.ReadUInt32()); 40 | if (value != searchId) 41 | { 42 | continue; 43 | } 44 | binaryReader.BaseStream.Position = offset; 45 | result = Helpers.ByteToType(binaryReader); 46 | return true; 47 | } 48 | return false; 49 | } 50 | 51 | private static byte[] ExtractXsrc(byte[] xsrcData) 52 | { 53 | using (var xsrcStream = new MemoryStream(xsrcData)) 54 | using (var xsrcReader = new BinaryReader(xsrcStream)) 55 | { 56 | 57 | var xsrcHeader = Helpers.ByteToType(xsrcReader); 58 | 59 | var magic = UnicodeHelper.GetUtf8String(xsrcHeader.Magic); 60 | if (magic.Equals("XSRC") == false) 61 | { 62 | return Array.Empty(); 63 | } 64 | 65 | var fileNameLen = Helpers.ConvertEndian(xsrcHeader.FileNameLen); 66 | var fileNameData = xsrcReader.ReadBytes((int)fileNameLen); 67 | var fileName = UnicodeHelper.GetUtf8String(fileNameData); 68 | 69 | var xsrcBody = Helpers.ByteToType(xsrcReader); 70 | var decompressedSize = Helpers.ConvertEndian(xsrcBody.DecompressedSize); 71 | var compressedSize = Helpers.ConvertEndian(xsrcBody.CompressedSize); 72 | 73 | var compData = xsrcReader.ReadBytes((int)compressedSize); 74 | var xmlData = new byte[decompressedSize]; 75 | 76 | using (var decompressor = new LibDeflate.GzipDecompressor()) 77 | { 78 | if (decompressor.Decompress(compData, xmlData, out var bytesWritten) != System.Buffers.OperationStatus.Done && bytesWritten != decompressedSize) 79 | { 80 | return Array.Empty(); 81 | } 82 | return xmlData.ToArray(); 83 | } 84 | } 85 | } 86 | 87 | private static string GetLocalizedElementString(XDocument document, XmlNamespaceManager namespaceManager, string id, string defaultValue) 88 | { 89 | var elements = document.XPathSelectElements($"/xlast:XboxLiveSubmissionProject/xlast:GameConfigProject/xlast:LocalizedStrings/xlast:LocalizedString[@id='{id}']/xlast:Translation", namespaceManager).ToArray(); 90 | if (elements == null || elements.Length == 0) 91 | { 92 | return defaultValue; 93 | } 94 | 95 | const string desiredLanguage = "en-US"; 96 | 97 | var result = elements[0].Value; 98 | for (int i = 0; i < elements.Length; i++) 99 | { 100 | var locale = elements[i]?.FirstAttribute?.Value; 101 | if (string.Equals(locale, desiredLanguage) == true) 102 | { 103 | result = elements[i].Value; 104 | break; 105 | } 106 | } 107 | 108 | return result; 109 | } 110 | 111 | private static bool TryDecrypt(XexContext context, byte[] xexKey, byte[] input, out byte[] result) 112 | { 113 | result = Array.Empty(); 114 | try 115 | { 116 | using (var aes1 = Aes.Create()) 117 | { 118 | aes1.Padding = PaddingMode.None; 119 | aes1.Key = xexKey; 120 | aes1.IV = new byte[16]; 121 | using (var aes1Decryptor = aes1.CreateDecryptor()) 122 | { 123 | var imageKey = context.SecurityInfo.ImageInfo.ImageKey; 124 | var decryptedKey = aes1Decryptor.TransformFinalBlock(imageKey, 0, imageKey.Length); 125 | if (decryptedKey == null) 126 | { 127 | return false; 128 | } 129 | 130 | var blockMultiple = Helpers.RoundToMultiple((uint)input.Length, 16); 131 | var paddingNeeded = blockMultiple - input.Length; 132 | var decryptBuffer = paddingNeeded > 0 ? new byte[input.Length + paddingNeeded] : input; 133 | if (paddingNeeded > 0) 134 | { 135 | Array.Copy(input, decryptBuffer, input.Length); 136 | } 137 | 138 | using (var aes2 = Aes.Create()) 139 | { 140 | aes2.Padding = PaddingMode.None; 141 | aes2.Key = decryptedKey; 142 | aes2.IV = new byte[16]; 143 | using (var aes2Decryptor = aes2.CreateDecryptor()) 144 | { 145 | var tempResult = aes2Decryptor.TransformFinalBlock(decryptBuffer, 0, decryptBuffer.Length); 146 | if (paddingNeeded > 0) 147 | { 148 | result = new byte[input.Length]; 149 | Array.Copy(tempResult, result, result.Length); 150 | } 151 | else 152 | { 153 | result = tempResult; 154 | } 155 | } 156 | } 157 | } 158 | } 159 | return true; 160 | } 161 | catch 162 | { 163 | return false; 164 | } 165 | } 166 | 167 | private static bool TryProcessRawData(XexContext context, BinaryReader reader, byte[] data, out byte[] result) 168 | { 169 | result = Array.Empty(); 170 | try 171 | { 172 | var fileDataSize = Helpers.ConvertEndian(context.FileDataDescriptor.Size); 173 | var fileDataCount = (fileDataSize - Helpers.SizeOf()) / Helpers.SizeOf(); 174 | 175 | var imageSize = Helpers.ConvertEndian(context.SecurityInfo.ImageSize); 176 | var rawData = new byte[imageSize]; 177 | 178 | var rawOffset = 0; 179 | var dataOffset = 0; 180 | for (var i = 0; i < fileDataCount; i++) 181 | { 182 | var rawDescriptor = Helpers.ByteToType(reader); 183 | var rawDataSize = Helpers.ConvertEndian(rawDescriptor.DataSize); 184 | var rawZeroSize = Helpers.ConvertEndian(rawDescriptor.ZeroSize); 185 | if (rawDataSize > 0) 186 | { 187 | Array.Copy(data, dataOffset, rawData, rawOffset, (int)rawDataSize); 188 | dataOffset += (int)rawDataSize; 189 | rawOffset += (int)rawDataSize; 190 | } 191 | if (rawZeroSize > 0) 192 | { 193 | rawOffset += (int)rawZeroSize; 194 | } 195 | } 196 | 197 | result = rawData; 198 | return true; 199 | } 200 | catch 201 | { 202 | return false; 203 | } 204 | } 205 | 206 | private static bool TryDecompressData(XexContext context, BinaryReader reader, byte[] data, out byte[] result) 207 | { 208 | result = Array.Empty(); 209 | try 210 | { 211 | var compressedDescriptor = Helpers.ByteToType(reader); 212 | var windowSize = Helpers.ConvertEndian(compressedDescriptor.WindowSize); 213 | var firstSize = Helpers.ConvertEndian(compressedDescriptor.Size); 214 | var imageSize = Helpers.ConvertEndian(context.SecurityInfo.ImageSize); 215 | 216 | if (XexUnpack.UnpackXexData(data, imageSize, windowSize, firstSize, out var unpacked) == false) 217 | { 218 | return false; 219 | } 220 | 221 | result = unpacked; 222 | return true; 223 | } 224 | catch 225 | { 226 | return false; 227 | } 228 | } 229 | public static bool TryCalculateChecksum(byte[] data, out string hash) 230 | { 231 | hash = string.Empty; 232 | using (var sha = SHA256.Create()) 233 | { 234 | sha.TransformFinalBlock(data, 0, data.Length); 235 | var sha256Hash = sha.Hash; 236 | if (sha256Hash == null) 237 | { 238 | return false; 239 | } 240 | hash = BitConverter.ToString(sha256Hash).Replace("-", string.Empty); 241 | return true; 242 | } 243 | } 244 | 245 | private struct XexContext 246 | { 247 | public XexHeader Header; 248 | public XexSecurityInfo SecurityInfo; 249 | public XexExecution Execution; 250 | public XexFileDataDescriptor FileDataDescriptor; 251 | } 252 | 253 | public static bool TryExtractXexMetaData(byte[] xexData, out XexMetaData metaData, string? checksum = null) 254 | { 255 | metaData = new XexMetaData 256 | { 257 | GameRegion = 0, 258 | TitleId = 0, 259 | MediaId = 0, 260 | Version = 0, 261 | BaseVersion = 0, 262 | DiscNum = 0, 263 | DiscTotal = 0, 264 | Thumbnail = Array.Empty(), 265 | TitleName = string.Empty, 266 | Description = string.Empty, 267 | Publisher = string.Empty, 268 | Developer = string.Empty, 269 | Genre = string.Empty 270 | }; 271 | 272 | try 273 | { 274 | XexContext xexContext = new XexContext(); 275 | 276 | using (var xexStream = new MemoryStream(xexData)) 277 | using (var xexReader = new BinaryReader(xexStream)) 278 | { 279 | 280 | if (xexData.Length < Helpers.SizeOf()) 281 | { 282 | System.Diagnostics.Debug.Print("Invalid file length for XexHeader structure."); 283 | return false; 284 | } 285 | 286 | xexContext.Header = Helpers.ByteToType(xexReader); 287 | 288 | var magic = UnicodeHelper.GetUtf8String(xexContext.Header.Magic); 289 | if (magic.Equals("XEX2") == false) 290 | { 291 | System.Diagnostics.Debug.Print("Invalid XEX header magic."); 292 | return false; 293 | } 294 | 295 | var securityInfoPos = Helpers.ConvertEndian(xexContext.Header.SecurityInfo); 296 | if (securityInfoPos > xexData.Length - Helpers.SizeOf()) 297 | { 298 | System.Diagnostics.Debug.Print("Invalid file length for XexSecurityInfo structure."); 299 | return false; 300 | } 301 | 302 | xexReader.BaseStream.Position = securityInfoPos; 303 | 304 | xexContext.SecurityInfo = Helpers.ByteToType(xexReader); 305 | 306 | var regions = Helpers.ConvertEndian(xexContext.SecurityInfo.ImageInfo.GameRegion); 307 | if ((regions & 0x000000FF) == 0x000000FF) 308 | { 309 | metaData.GameRegion |= XexRegion.USA; 310 | } 311 | if ((regions & 0x0000FD00) == 0x0000FD00) 312 | { 313 | metaData.GameRegion |= XexRegion.Japan; 314 | } 315 | if ((regions & 0x00FF0000) == 0x00FF0000) 316 | { 317 | metaData.GameRegion |= XexRegion.Europe; 318 | } 319 | 320 | var xexExecutionSearchId = (XexExecutionId << 8) | (uint)(Helpers.SizeOf() >> 2); 321 | if (SearchField(xexReader, xexContext.Header, xexExecutionSearchId, out xexContext.Execution) == false) 322 | { 323 | System.Diagnostics.Debug.Print("Unable to find XexExecution structure."); 324 | return false; 325 | } 326 | 327 | metaData.TitleId = Helpers.ConvertEndian(xexContext.Execution.TitleId); 328 | metaData.MediaId = Helpers.ConvertEndian(xexContext.Execution.MediaId); 329 | metaData.Version = Helpers.ConvertEndian(xexContext.Execution.Version); 330 | metaData.BaseVersion = Helpers.ConvertEndian(xexContext.Execution.BaseVersion); 331 | metaData.DiscNum = xexContext.Execution.DiscNum; 332 | metaData.DiscTotal = xexContext.Execution.DiscTotal; 333 | 334 | if (checksum != null) 335 | { 336 | metaData.Checksum = checksum; 337 | } 338 | else if (TryCalculateChecksum(xexData, out var checksumValue) == true) 339 | { 340 | metaData.Checksum = checksumValue; 341 | } 342 | 343 | var xexFileDataDescriptorSearchId = (XexFileDataDescriptorId << 8) | 0xff; 344 | if (SearchField(xexReader, xexContext.Header, xexFileDataDescriptorSearchId, out xexContext.FileDataDescriptor) == false) 345 | { 346 | System.Diagnostics.Debug.Print("Skipping detailed xex info due to being unable to find XexFileDataDescriptor structure."); 347 | return true; 348 | } 349 | 350 | var fileDataDescriptorPos = xexReader.BaseStream.Position; 351 | var dataPos = Helpers.ConvertEndian(xexContext.Header.SizeOfHeaders); 352 | var dataLen = xexData.Length - Helpers.ConvertEndian(xexContext.Header.SizeOfHeaders); 353 | xexReader.BaseStream.Position = dataPos; 354 | var data = xexReader.ReadBytes((int)dataLen); 355 | 356 | var processed = false; 357 | var decryptionKeys = new byte[][] { XexRetailKey, XexDevkitKey }; 358 | 359 | foreach (var decryptionKey in decryptionKeys) 360 | { 361 | xexReader.BaseStream.Position = fileDataDescriptorPos; 362 | 363 | var processedData = data; 364 | 365 | var flags = Helpers.ConvertEndian(xexContext.FileDataDescriptor.Flags); 366 | 367 | if ((flags & XexDataFlagEncrypted) == XexDataFlagEncrypted) 368 | { 369 | if (TryDecrypt(xexContext, decryptionKey, processedData, out processedData) == false) 370 | { 371 | continue; 372 | } 373 | } 374 | else 375 | { 376 | processedData = data; 377 | } 378 | 379 | var format = Helpers.ConvertEndian(xexContext.FileDataDescriptor.Format); 380 | if (format == XexDataFormatRaw) 381 | { 382 | if (TryProcessRawData(xexContext, xexReader, processedData, out processedData) == false) 383 | { 384 | continue; 385 | } 386 | } 387 | else if (format == XexDataFormatCompressed) 388 | { 389 | if (TryDecompressData(xexContext, xexReader, processedData, out processedData) == false) 390 | { 391 | continue; 392 | } 393 | 394 | } 395 | else if (format == XexDataFormatDeltaCompressed) 396 | { 397 | System.Diagnostics.Debug.Print("Unsupported format 'XexDataFormatDeltaCompressed'."); 398 | return false; 399 | } 400 | else 401 | { 402 | System.Diagnostics.Debug.Print($"Unrecognized format value {format}."); 403 | return false; 404 | } 405 | 406 | processed = processedData[0] == 'M' && processedData[1] == 'Z'; 407 | if (processed) 408 | { 409 | data = processedData; 410 | break; 411 | } 412 | } 413 | 414 | if (processed == false) 415 | { 416 | System.Diagnostics.Debug.Print("Unable to process xex data."); 417 | return false; 418 | } 419 | 420 | xexReader.BaseStream.Position = fileDataDescriptorPos; 421 | 422 | var headerSectionTableSearchId = (XexHeaderSectionTableId << 8) | 0xff; 423 | if (SearchField(xexReader, xexContext.Header, headerSectionTableSearchId, out var headerSectionTable) == false) 424 | { 425 | System.Diagnostics.Debug.Print("Skipping detailed xex info due to being unable to find XexHeaderSectionTable structure."); 426 | return true; 427 | } 428 | 429 | var headerSectionSize = Helpers.ConvertEndian(headerSectionTable.Size); 430 | var headerSectionCount = headerSectionSize / Helpers.SizeOf(); 431 | for (var i = 0; i < headerSectionCount; i++) 432 | { 433 | var headerSectionEntry = Helpers.ByteToType(xexReader); 434 | var headerSectionName = UnicodeHelper.GetUtf8String(headerSectionEntry.SectionName); 435 | var headerSearchTitle = $"{Helpers.ConvertEndian(xexContext.Execution.TitleId):X}"; 436 | if (headerSectionName.Equals(headerSearchTitle)) 437 | { 438 | var virtualSize = Helpers.ConvertEndian(headerSectionEntry.VirtualSize); 439 | var virtualAddress = Helpers.ConvertEndian(headerSectionEntry.VirtualAddress); 440 | 441 | using (var dataStream = new MemoryStream(data)) 442 | using (var dataReader = new BinaryReader(dataStream)) 443 | { 444 | 445 | var xdbfPosition = virtualAddress - Helpers.ConvertEndian(xexContext.SecurityInfo.ImageInfo.LoadAddress); 446 | 447 | dataStream.Position = xdbfPosition; 448 | 449 | var xdbfHeader = Helpers.ByteToType(dataReader); 450 | var xdbfMagic = UnicodeHelper.GetUtf8String(xdbfHeader.Magic); 451 | if (xdbfMagic.Equals("XDBF") == false) 452 | { 453 | System.Diagnostics.Debug.Print("Invalid XDBF header magic."); 454 | return false; 455 | } 456 | 457 | var entryCount = Helpers.ConvertEndian(xdbfHeader.EntryCount); 458 | var entrySize = entryCount * Helpers.SizeOf(); 459 | if (xdbfPosition + entrySize >= data.Length) 460 | { 461 | System.Diagnostics.Debug.Print("Invalid XDBF length for XDBF entries."); 462 | return false; 463 | } 464 | 465 | var baseOffset = xdbfPosition + (Helpers.ConvertEndian(xdbfHeader.EntryTableLen) * Helpers.SizeOf()) + (Helpers.ConvertEndian(xdbfHeader.freeMemTablLen) * 8) + Helpers.SizeOf(); 466 | if (baseOffset >= data.Length) 467 | { 468 | System.Diagnostics.Debug.Print("Invalid XDBF length for XDBF entries."); 469 | return false; 470 | } 471 | 472 | 473 | for (var j = 0; j < entryCount; j++) 474 | { 475 | var xdbfEntry = Helpers.ByteToType(dataReader); 476 | var entryType = Helpers.ConvertEndian(xdbfEntry.Type); 477 | var entryOffset = baseOffset + Helpers.ConvertEndian(xdbfEntry.Offset); 478 | var entryLength = Helpers.ConvertEndian(xdbfEntry.Length); 479 | var entryIdentifier1 = Helpers.ConvertEndian(xdbfEntry.Identifier1); 480 | var entryIdentifier2 = Helpers.ConvertEndian(xdbfEntry.Identifier2); 481 | 482 | if (entryType == 3 && entryIdentifier1 == 0 && entryIdentifier2 == 1) 483 | { 484 | var tempPosition = dataStream.Position; 485 | dataStream.Position = entryOffset; 486 | 487 | var xstrHeader = Helpers.ByteToType(dataReader); 488 | if (UnicodeHelper.GetUtf8String(xstrHeader.Magic) == "XSTR") 489 | { 490 | var xstrSize = Helpers.ConvertEndian(xstrHeader.Size); 491 | var xstrEntryCount = Helpers.ConvertEndian(xstrHeader.EntryCount); 492 | 493 | for (i = 0; i < xstrEntryCount; i++) 494 | { 495 | var xstrType = Helpers.ConvertEndian(dataReader.ReadUInt16()); 496 | var xstrLen = Helpers.ConvertEndian(dataReader.ReadUInt16()); 497 | var xstrrValue = UnicodeHelper.GetUtf8String(dataReader.ReadBytes(xstrLen)); 498 | if (xstrType == 32768) 499 | { 500 | metaData.TitleName = xstrrValue; 501 | } 502 | } 503 | } 504 | } 505 | else if (entryType == 2 && entryIdentifier1 == 0 && entryIdentifier2 == 0x8000) 506 | { 507 | var tempPosition = dataStream.Position; 508 | dataStream.Position = entryOffset; 509 | metaData.Thumbnail = dataReader.ReadBytes((int)entryLength); 510 | dataStream.Position = tempPosition; 511 | } 512 | else if (entryType == 1 && entryIdentifier1 == 0 && entryIdentifier2 == XSRC) 513 | { 514 | var tempPosition = dataStream.Position; 515 | dataStream.Position = entryOffset; 516 | var xsrcData = ExtractXsrc(dataReader.ReadBytes((int)entryLength)); 517 | 518 | dataStream.Position = tempPosition; 519 | 520 | var namespaceManager = new XmlNamespaceManager(new NameTable()); 521 | namespaceManager.AddNamespace("xlast", "http://www.xboxlive.com/xlast"); 522 | 523 | var documentUnicode = UnicodeHelper.GetUnicodeString(xsrcData); 524 | var xboxLiveSubmissionDocument = XDocument.Parse(documentUnicode); 525 | var gameConfigProjectElement = xboxLiveSubmissionDocument.XPathSelectElement("/xlast:XboxLiveSubmissionProject/xlast:GameConfigProject", namespaceManager); 526 | if (gameConfigProjectElement != null) 527 | { 528 | var titleNameAttribue = gameConfigProjectElement.Attribute(XName.Get("titleName")); 529 | if (titleNameAttribue != null) 530 | { 531 | metaData.TitleName = GetLocalizedElementString(xboxLiveSubmissionDocument, namespaceManager, "32768", metaData.TitleName); 532 | } 533 | } 534 | 535 | var productInformationElement = xboxLiveSubmissionDocument.XPathSelectElement("/xlast:XboxLiveSubmissionProject/xlast:GameConfigProject/xlast:ProductInformation", namespaceManager); 536 | if (productInformationElement != null) 537 | { 538 | var sellTextStringIdAttribue = productInformationElement.Attribute(XName.Get("sellTextStringId")); 539 | if (sellTextStringIdAttribue != null) 540 | { 541 | //<Translated text> 542 | metaData.Description = GetLocalizedElementString(xboxLiveSubmissionDocument, namespaceManager, sellTextStringIdAttribue.Value, sellTextStringIdAttribue.Value); 543 | } 544 | 545 | var publisherStringIdAttribue = productInformationElement.Attribute(XName.Get("publisherStringId")); 546 | if (publisherStringIdAttribue != null) 547 | { 548 | metaData.Publisher = GetLocalizedElementString(xboxLiveSubmissionDocument, namespaceManager, publisherStringIdAttribue.Value, publisherStringIdAttribue.Value); 549 | } 550 | 551 | var developerStringIdAttribue = productInformationElement.Attribute(XName.Get("developerStringId")); 552 | if (developerStringIdAttribue != null) 553 | { 554 | metaData.Developer = GetLocalizedElementString(xboxLiveSubmissionDocument, namespaceManager, developerStringIdAttribue.Value, developerStringIdAttribue.Value); 555 | } 556 | 557 | var genreTextStringIdAttribue = productInformationElement.Attribute(XName.Get("genreTextStringId")); 558 | if (genreTextStringIdAttribue != null) 559 | { 560 | metaData.Genre = GetLocalizedElementString(xboxLiveSubmissionDocument, namespaceManager, genreTextStringIdAttribue.Value, genreTextStringIdAttribue.Value); 561 | } 562 | } 563 | } 564 | } 565 | } 566 | } 567 | } 568 | 569 | } 570 | return true; 571 | } 572 | catch (Exception ex) 573 | { 574 | System.Diagnostics.Debug.Print($"Exception occurred: {ex}"); 575 | return false; 576 | } 577 | } 578 | } 579 | } 580 | --------------------------------------------------------------------------------