From 761b418d21b87396ac0a4a8f2c3d0b304a18e6ac Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sat, 7 Jan 2023 12:33:15 -0800 Subject: [PATCH] Fix DS/3DS sttuff, add 3DS wrapper/printing --- BurnOutSharp.Builders/N3DS.cs | 160 +++++-- BurnOutSharp.Builders/Nitro.cs | 4 +- BurnOutSharp.Models/N3DS/CardInfoHeader.cs | 61 +++ BurnOutSharp.Models/N3DS/Cart.cs | 10 + .../N3DS/DevelopmentCardInfoHeader.cs | 31 ++ BurnOutSharp.Models/N3DS/InitialData.cs | 35 ++ BurnOutSharp.Models/N3DS/NCSDHeader.cs | 84 +--- BurnOutSharp.Models/N3DS/TestData.cs | 59 +++ BurnOutSharp.Wrappers/N3DS.cs | 452 ++++++++++++++++++ 9 files changed, 783 insertions(+), 113 deletions(-) create mode 100644 BurnOutSharp.Models/N3DS/CardInfoHeader.cs create mode 100644 BurnOutSharp.Models/N3DS/DevelopmentCardInfoHeader.cs create mode 100644 BurnOutSharp.Models/N3DS/InitialData.cs create mode 100644 BurnOutSharp.Models/N3DS/TestData.cs create mode 100644 BurnOutSharp.Wrappers/N3DS.cs diff --git a/BurnOutSharp.Builders/N3DS.cs b/BurnOutSharp.Builders/N3DS.cs index 0ea02d59..32ac33e6 100644 --- a/BurnOutSharp.Builders/N3DS.cs +++ b/BurnOutSharp.Builders/N3DS.cs @@ -56,7 +56,7 @@ namespace BurnOutSharp.Builders // Create a new cart image to fill var cart = new Cart(); - #region Header + #region NCSD Header // Try to parse the header var header = ParseNCSDHeader(data); @@ -68,6 +68,34 @@ namespace BurnOutSharp.Builders #endregion + #region Card Info Header + + // Try to parse the card info header + var cardInfoHeader = ParseCardInfoHeader(data); + if (cardInfoHeader == null) + return null; + + // Set the card info header + cart.CardInfoHeader = cardInfoHeader; + + #endregion + + #region Development Card Info Header + + // TODO: Hook up a builder flag to enable development parsing + if (false) + { + // Try to parse the development card info header + var developmentCardInfoHeader = ParseDevelopmentCardInfoHeader(data); + if (developmentCardInfoHeader == null) + return null; + + // Set the development card info header + cart.DevelopmentCardInfoHeader = developmentCardInfoHeader; + } + + #endregion + #region Partitions // Create the partition table @@ -88,16 +116,15 @@ namespace BurnOutSharp.Builders /// Parse a Stream into an NCSD header /// /// Stream to parse - /// Indicates if the cart is development or not /// Filled NCSD header on success, null on error - private static NCSDHeader ParseNCSDHeader(Stream data, bool development = false) + private static NCSDHeader ParseNCSDHeader(Stream data) { // TODO: Use marshalling here instead of building NCSDHeader header = new NCSDHeader(); header.RSA2048Signature = data.ReadBytes(0x100); byte[] magicNumber = data.ReadBytes(4); - header.MagicNumber = Encoding.ASCII.GetString(magicNumber); + header.MagicNumber = Encoding.ASCII.GetString(magicNumber).TrimEnd('\0');; if (header.MagicNumber != NCSDMagicNumber) return null; @@ -119,36 +146,16 @@ namespace BurnOutSharp.Builders header.SectorZeroOffset = data.ReadUInt32(); header.PartitionFlags = data.ReadBytes(8); - header.PartitionIdTable = new byte[8][]; + header.PartitionIdTable = new ulong[8]; for (int i = 0; i < 8; i++) { - header.PartitionIdTable[i] = data.ReadBytes(8); + header.PartitionIdTable[i] = data.ReadUInt64(); } header.Reserved1 = data.ReadBytes(0x20); - header.Reserved2 = data.ReadBytes(0xE); + header.Reserved2 = data.ReadBytes(0x0E); header.FirmUpdateByte1 = data.ReadByteValue(); header.FirmUpdateByte2 = data.ReadByteValue(); - - header.CARD2WritableAddressMediaUnits = data.ReadBytes(4); - header.CardInfoBytemask = data.ReadBytes(4); - header.Reserved3 = data.ReadBytes(0x108); - header.TitleVersion = data.ReadUInt16(); - header.CardRevision = data.ReadUInt16(); - header.Reserved4 = data.ReadBytes(0xCEC); // Incorrectly documented as 0xCEE - header.CardSeedKeyY = data.ReadBytes(0x10); - header.EncryptedCardSeed = data.ReadBytes(0x10); - header.CardSeedAESMAC = data.ReadBytes(0x10); - header.CardSeedNonce = data.ReadBytes(0xC); - header.Reserved5 = data.ReadBytes(0xC4); - header.BackupHeader = ParseNCCHHeader(data, true); - - if (development) - { - header.CardDeviceReserved1 = data.ReadBytes(0x200); - header.TitleKey = data.ReadBytes(0x10); - header.CardDeviceReserved2 = data.ReadBytes(0xF0); - } } else if (header.PartitionsFSType == FilesystemType.FIRM) { @@ -175,6 +182,78 @@ namespace BurnOutSharp.Builders return partitionTableEntry; } + /// + /// Parse a Stream into a card info header + /// + /// Stream to parse + /// Filled card info header on success, null on error + private static CardInfoHeader ParseCardInfoHeader(Stream data) + { + // TODO: Use marshalling here instead of building + CardInfoHeader cardInfoHeader = new CardInfoHeader(); + + cardInfoHeader.WritableAddressMediaUnits = data.ReadUInt32(); + cardInfoHeader.CardInfoBitmask = data.ReadUInt32(); + cardInfoHeader.Reserved1 = data.ReadBytes(0xF8); + cardInfoHeader.FilledSize = data.ReadUInt32(); + cardInfoHeader.Reserved2 = data.ReadBytes(0x0C); + cardInfoHeader.TitleVersion = data.ReadUInt16(); + cardInfoHeader.CardRevision = data.ReadUInt16(); + cardInfoHeader.Reserved3 = data.ReadBytes(0x0C); + cardInfoHeader.CVerTitleID = data.ReadBytes(8); + cardInfoHeader.CVerVersionNumber = data.ReadUInt16(); + cardInfoHeader.Reserved4 = data.ReadBytes(0xCD6); + + return cardInfoHeader; + } + + /// + /// Parse a Stream into a development card info header + /// + /// Stream to parse + /// Filled development card info header on success, null on error + private static DevelopmentCardInfoHeader ParseDevelopmentCardInfoHeader(Stream data) + { + // TODO: Use marshalling here instead of building + DevelopmentCardInfoHeader developmentCardInfoHeader = new DevelopmentCardInfoHeader(); + + developmentCardInfoHeader.InitialData = ParseInitialData(data); + if (developmentCardInfoHeader.InitialData == null) + return null; + + developmentCardInfoHeader.CardDeviceReserved1 = data.ReadBytes(0x200); + developmentCardInfoHeader.TitleKey = data.ReadBytes(0x10); + developmentCardInfoHeader.CardDeviceReserved2 = data.ReadBytes(0x1BF0); + + developmentCardInfoHeader.TestData = ParseTestData(data); + if (developmentCardInfoHeader.TestData == null) + return null; + + return developmentCardInfoHeader; + } + + /// + /// Parse a Stream into an initial data + /// + /// Stream to parse + /// Filled initial data on success, null on error + private static InitialData ParseInitialData(Stream data) + { + // TODO: Use marshalling here instead of building + InitialData initialData = new InitialData(); + + initialData.CardSeedKeyY = data.ReadBytes(0x10); + initialData.EncryptedCardSeed = data.ReadBytes(0x10); + initialData.CardSeedAESMAC = data.ReadBytes(0x10); + initialData.CardSeedNonce = data.ReadBytes(0xC); + initialData.Reserved = data.ReadBytes(0xC4); + initialData.BackupHeader = ParseNCCHHeader(data, true); + if (initialData.BackupHeader == null) + return null; + + return initialData; + } + /// /// Parse a Stream into an NCCH header /// @@ -190,7 +269,7 @@ namespace BurnOutSharp.Builders header.RSA2048Signature = data.ReadBytes(0x100); byte[] magicId = data.ReadBytes(4); - header.MagicID = Encoding.ASCII.GetString(magicId); + header.MagicID = Encoding.ASCII.GetString(magicId).TrimEnd('\0');; if (header.MagicID != NCCHMagicNumber) return null; @@ -247,6 +326,31 @@ namespace BurnOutSharp.Builders return headerFlags; } + /// + /// Parse a Stream into an initial data + /// + /// Stream to parse + /// Filled initial data on success, null on error + private static TestData ParseTestData(Stream data) + { + // TODO: Use marshalling here instead of building + TestData testData = new TestData(); + + // TODO: Validate some of the values + testData.Signature = data.ReadBytes(8); + testData.AscendingByteSequence = data.ReadBytes(0x1F8); + testData.DescendingByteSequence = data.ReadBytes(0x200); + testData.Filled00 = data.ReadBytes(0x200); + testData.FilledFF = data.ReadBytes(0x200); + testData.Filled0F = data.ReadBytes(0x200); + testData.FilledF0 = data.ReadBytes(0x200); + testData.Filled55 = data.ReadBytes(0x200); + testData.FilledAA = data.ReadBytes(0x1FF); + testData.FinalByte = data.ReadByteValue(); + + return testData; + } + #endregion } } diff --git a/BurnOutSharp.Builders/Nitro.cs b/BurnOutSharp.Builders/Nitro.cs index 030f7934..46ee3878 100644 --- a/BurnOutSharp.Builders/Nitro.cs +++ b/BurnOutSharp.Builders/Nitro.cs @@ -113,10 +113,10 @@ namespace BurnOutSharp.Builders CommonHeader commonHeader = new CommonHeader(); byte[] gameTitle = data.ReadBytes(0x0C); - commonHeader.GameTitle = Encoding.ASCII.GetString(gameTitle); + commonHeader.GameTitle = Encoding.ASCII.GetString(gameTitle).TrimEnd('\0'); commonHeader.GameCode = data.ReadUInt32(); byte[] makerCode = data.ReadBytes(2); - commonHeader.MakerCode = Encoding.ASCII.GetString(bytes: makerCode); + commonHeader.MakerCode = Encoding.ASCII.GetString(bytes: makerCode).TrimEnd('\0'); commonHeader.UnitCode = (Unitcode)data.ReadByteValue(); commonHeader.EncryptionSeedSelect = data.ReadByteValue(); commonHeader.DeviceCapacity = data.ReadByteValue(); diff --git a/BurnOutSharp.Models/N3DS/CardInfoHeader.cs b/BurnOutSharp.Models/N3DS/CardInfoHeader.cs new file mode 100644 index 00000000..4e3ca216 --- /dev/null +++ b/BurnOutSharp.Models/N3DS/CardInfoHeader.cs @@ -0,0 +1,61 @@ +namespace BurnOutSharp.Models.N3DS +{ + /// + public sealed class CardInfoHeader + { + /// + /// CARD2: Writable Address In Media Units (For 'On-Chip' Savedata). CARD1: Always 0xFFFFFFFF. + /// + public uint WritableAddressMediaUnits; + + /// + /// Card Info Bitmask + /// + public uint CardInfoBitmask; + + /// + /// Reserved + /// + public byte[] Reserved1; + + /// + /// Filled size of cartridge + /// + public uint FilledSize; + + /// + /// Reserved + /// + public byte[] Reserved2; + + /// + /// Title version + /// + public ushort TitleVersion; + + /// + /// Card revision + /// + public ushort CardRevision; + + /// + /// Reserved + /// + public byte[] Reserved3; + + /// + /// Title ID of CVer in included update partition + /// + public byte[] CVerTitleID; + + /// + /// Version number of CVer in included update partition + /// + public ushort CVerVersionNumber; + + /// + /// Reserved + /// + public byte[] Reserved4; + } +} diff --git a/BurnOutSharp.Models/N3DS/Cart.cs b/BurnOutSharp.Models/N3DS/Cart.cs index 85fe08fe..98496919 100644 --- a/BurnOutSharp.Models/N3DS/Cart.cs +++ b/BurnOutSharp.Models/N3DS/Cart.cs @@ -10,6 +10,16 @@ namespace BurnOutSharp.Models.N3DS /// public NCSDHeader Header { get; set; } + /// + /// 3DS card info header + /// + public CardInfoHeader CardInfoHeader { get; set; } + + /// + /// 3DS development card info header + /// + public DevelopmentCardInfoHeader DevelopmentCardInfoHeader { get; set; } + /// /// NCCH partitions /// diff --git a/BurnOutSharp.Models/N3DS/DevelopmentCardInfoHeader.cs b/BurnOutSharp.Models/N3DS/DevelopmentCardInfoHeader.cs new file mode 100644 index 00000000..9c7429d6 --- /dev/null +++ b/BurnOutSharp.Models/N3DS/DevelopmentCardInfoHeader.cs @@ -0,0 +1,31 @@ +namespace BurnOutSharp.Models.N3DS +{ + /// + public sealed class DevelopmentCardInfoHeader + { + /// + /// InitialData + /// + public InitialData InitialData; + + /// + /// CardDeviceReserved1 + /// + public byte[] CardDeviceReserved1; + + /// + /// TitleKey + /// + public byte[] TitleKey; + + /// + /// CardDeviceReserved2 + /// + public byte[] CardDeviceReserved2; + + /// + /// TestData + /// + public TestData TestData; + } +} diff --git a/BurnOutSharp.Models/N3DS/InitialData.cs b/BurnOutSharp.Models/N3DS/InitialData.cs new file mode 100644 index 00000000..9415671c --- /dev/null +++ b/BurnOutSharp.Models/N3DS/InitialData.cs @@ -0,0 +1,35 @@ +namespace BurnOutSharp.Models.N3DS +{ + /// + public sealed class InitialData + { + /// + /// Card seed keyY (first u64 is Media ID (same as first NCCH partitionId)) + /// + public byte[] CardSeedKeyY; + + /// + /// Encrypted card seed (AES-CCM, keyslot 0x3B for retail cards, see CTRCARD_SECSEED) /// + public byte[] EncryptedCardSeed; + + /// + /// Card seed AES-MAC + /// + public byte[] CardSeedAESMAC; + + /// + /// Card seed nonce + /// + public byte[] CardSeedNonce; + + /// + /// Reserved3 + /// + public byte[] Reserved; + + /// + /// Copy of first NCCH header (excluding RSA signature) + /// + public NCCHHeader BackupHeader; + } +} diff --git a/BurnOutSharp.Models/N3DS/NCSDHeader.cs b/BurnOutSharp.Models/N3DS/NCSDHeader.cs index 87018d07..9d5142ce 100644 --- a/BurnOutSharp.Models/N3DS/NCSDHeader.cs +++ b/BurnOutSharp.Models/N3DS/NCSDHeader.cs @@ -75,7 +75,7 @@ /// /// Partition ID table /// - public byte[][] PartitionIdTable; + public ulong[] PartitionIdTable; /// /// Reserved @@ -115,87 +115,5 @@ public byte[] EncryptedMBR; #endregion - - #region Card Info Header - - /// - /// CARD2: Writable Address In Media Units (For 'On-Chip' Savedata). CARD1: Always 0xFFFFFFFF. - /// - public byte[] CARD2WritableAddressMediaUnits; - - /// - /// Card Info Bitmask - /// - public byte[] CardInfoBytemask; - - /// - /// Reserved1 - /// - public byte[] Reserved3; - - /// - /// Title version - /// - public ushort TitleVersion; - - /// - /// Card revision - /// - public ushort CardRevision; - - /// - /// Reserved2 - /// - public byte[] Reserved4; - - /// - /// Card seed keyY (first u64 is Media ID (same as first NCCH partitionId)) - /// - public byte[] CardSeedKeyY; - - /// - /// Encrypted card seed (AES-CCM, keyslot 0x3B for retail cards, see CTRCARD_SECSEED) /// - public byte[] EncryptedCardSeed; - - /// - /// Card seed AES-MAC - /// - public byte[] CardSeedAESMAC; - - /// - /// Card seed nonce - /// - public byte[] CardSeedNonce; - - /// - /// Reserved3 - /// - public byte[] Reserved5; - - /// - /// Copy of first NCCH header (excluding RSA signature) - /// - public NCCHHeader BackupHeader; - - #endregion - - #region Development Card Info Header Extension - - /// - /// CardDeviceReserved1 - /// - public byte[] CardDeviceReserved1; - - /// - /// TitleKey - /// - public byte[] TitleKey; - - /// - /// CardDeviceReserved2 - /// - public byte[] CardDeviceReserved2; - - #endregion } } diff --git a/BurnOutSharp.Models/N3DS/TestData.cs b/BurnOutSharp.Models/N3DS/TestData.cs new file mode 100644 index 00000000..212816c9 --- /dev/null +++ b/BurnOutSharp.Models/N3DS/TestData.cs @@ -0,0 +1,59 @@ +namespace BurnOutSharp.Models.N3DS +{ + /// + /// The test data is the same one encountered in development DS/DSi cartridges. + /// + /// + public sealed class TestData + { + /// + /// The bytes FF 00 FF 00 AA 55 AA 55. + /// + public byte[] Signature; + + /// + /// An ascending byte sequence equal to the offset mod 256 (08 09 0A ... FE FF 00 01 ... FF). + /// + public byte[] AscendingByteSequence; + + /// + /// A descending byte sequence equal to 255 minus the offset mod 256 (FF FE FD ... 00 FF DE ... 00). + /// + public byte[] DescendingByteSequence; + + /// + /// Filled with 00 (0b00000000) bytes. + /// + public byte[] Filled00; + + /// + /// Filled with FF (0b11111111) bytes. + /// + public byte[] FilledFF; + + /// + /// Filled with 0F (0b00001111) bytes. + /// + public byte[] Filled0F; + + /// + /// Filled with F0 (0b11110000) bytes. + /// + public byte[] FilledF0; + + /// + /// Filled with 55 (0b01010101) bytes. + /// + public byte[] Filled55; + + /// + /// Filled with AA (0b10101010) bytes. + /// + public byte[] FilledAA; + + /// + /// The final byte is 00 (0b00000000). + /// + public byte FinalByte; + } +} diff --git a/BurnOutSharp.Wrappers/N3DS.cs b/BurnOutSharp.Wrappers/N3DS.cs new file mode 100644 index 00000000..d8371b1a --- /dev/null +++ b/BurnOutSharp.Wrappers/N3DS.cs @@ -0,0 +1,452 @@ +using System; +using System.IO; + +namespace BurnOutSharp.Wrappers +{ + public class N3DS : WrapperBase + { + #region Pass-Through Properties + + #region Header + + #region Common to all NCSD files + + /// + public byte[] RSA2048Signature => _cart.Header.RSA2048Signature; + + /// + public string MagicNumber => _cart.Header.MagicNumber; + + /// + public uint ImageSizeInMediaUnits => _cart.Header.ImageSizeInMediaUnits; + + /// + public byte[] MediaId => _cart.Header.MediaId; + + /// + public Models.N3DS.FilesystemType PartitionsFSType => _cart.Header.PartitionsFSType; + + /// + public byte[] PartitionsCryptType => _cart.Header.PartitionsCryptType; + + /// + public Models.N3DS.PartitionTableEntry[] PartitionsTable => _cart.Header.PartitionsTable; + + #endregion + + #region CTR Cart Image (CCI) Specific + + /// + public byte[] ExheaderHash => _cart.Header.ExheaderHash; + + /// + public uint AdditionalHeaderSize => _cart.Header.AdditionalHeaderSize; + + /// + public uint SectorZeroOffset => _cart.Header.SectorZeroOffset; + + /// + public byte[] PartitionFlags => _cart.Header.PartitionFlags; + + /// + public ulong[] PartitionIdTable => _cart.Header.PartitionIdTable; + + /// + public byte[] Reserved1 => _cart.Header.Reserved1; + + /// + public byte[] Reserved2 => _cart.Header.Reserved2; + + /// + public byte FirmUpdateByte1 => _cart.Header.FirmUpdateByte1; + + /// + public byte FirmUpdateByte2 => _cart.Header.FirmUpdateByte2; + + #endregion + + #region Raw NAND Format Specific + + /// + public byte[] Unknown => _cart.Header.Unknown; + + /// + public byte[] EncryptedMBR => _cart.Header.EncryptedMBR; + + #endregion + + #endregion + + #region Card Info Header + + /// + public uint CIH_WritableAddressMediaUnits => _cart.CardInfoHeader.WritableAddressMediaUnits; + + /// + public uint CIH_CardInfoBitmask => _cart.CardInfoHeader.CardInfoBitmask; + + /// + public byte[] CIH_Reserved1 => _cart.CardInfoHeader.Reserved1; + + /// + public uint CIH_FilledSize => _cart.CardInfoHeader.FilledSize; + + /// + public byte[] CIH_Reserved2 => _cart.CardInfoHeader.Reserved2; + + /// + public ushort CIH_TitleVersion => _cart.CardInfoHeader.TitleVersion; + + /// + public ushort CIH_CardRevision => _cart.CardInfoHeader.CardRevision; + + /// + public byte[] CIH_Reserved3 => _cart.CardInfoHeader.Reserved3; + + /// + public byte[] CIH_CVerTitleID => _cart.CardInfoHeader.CVerTitleID; + + /// + public ushort CIH_CVerVersionNumber => _cart.CardInfoHeader.CVerVersionNumber; + + /// + public byte[] CIH_Reserved4 => _cart.CardInfoHeader.Reserved4; + + #endregion + + #region Development Card Info Header + + #region Initial Data + + /// + public byte[] DCIH_ID_CardSeedKeyY => _cart.DevelopmentCardInfoHeader?.InitialData?.CardSeedKeyY; + + /// + public byte[] DCIH_ID_EncryptedCardSeed => _cart.DevelopmentCardInfoHeader?.InitialData?.EncryptedCardSeed; + + /// + public byte[] DCIH_ID_CardSeedAESMAC => _cart.DevelopmentCardInfoHeader?.InitialData?.CardSeedAESMAC; + + /// + public byte[] DCIH_ID_CardSeedNonce => _cart.DevelopmentCardInfoHeader?.InitialData?.CardSeedNonce; + + /// + public byte[] DCIH_ID_Reserved => _cart.DevelopmentCardInfoHeader?.InitialData?.Reserved; + + /// + public Models.N3DS.NCCHHeader DCIH_ID_BackupHeader => _cart.DevelopmentCardInfoHeader?.InitialData?.BackupHeader; + + #endregion + + /// + public byte[] DCIH_CardDeviceReserved1 => _cart.DevelopmentCardInfoHeader?.CardDeviceReserved1; + + /// + public byte[] DCIH_TitleKey => _cart.DevelopmentCardInfoHeader?.TitleKey; + + /// + public byte[] DCIH_CardDeviceReserved2 => _cart.DevelopmentCardInfoHeader?.CardDeviceReserved2; + + #region Test Data + + /// + public byte[] DCIH_TD_Signature => _cart.DevelopmentCardInfoHeader?.TestData?.Signature; + + /// + public byte[] DCIH_TD_AscendingByteSequence => _cart.DevelopmentCardInfoHeader?.TestData?.AscendingByteSequence; + + /// + public byte[] DCIH_TD_DescendingByteSequence => _cart.DevelopmentCardInfoHeader?.TestData?.DescendingByteSequence; + + /// + public byte[] DCIH_TD_Filled00 => _cart.DevelopmentCardInfoHeader?.TestData?.Filled00; + + /// + public byte[] DCIH_TD_FilledFF => _cart.DevelopmentCardInfoHeader?.TestData?.FilledFF; + + /// + public byte[] DCIH_TD_Filled0F => _cart.DevelopmentCardInfoHeader?.TestData?.Filled0F; + + /// + public byte[] DCIH_TD_FilledF0 => _cart.DevelopmentCardInfoHeader?.TestData?.FilledF0; + + /// + public byte[] DCIH_TD_Filled55 => _cart.DevelopmentCardInfoHeader?.TestData?.Filled55; + + /// + public byte[] DCIH_TD_FilledAA => _cart.DevelopmentCardInfoHeader?.TestData?.FilledAA; + + /// + public byte? DCIH_TD_FinalByte => _cart.DevelopmentCardInfoHeader?.TestData?.FinalByte; + + #endregion + + #endregion + + #region Partitions + + /// + public Models.N3DS.NCCHHeader[] Partitions => _cart.Partitions; + + #endregion + + #endregion + + #region Instance Variables + + /// + /// Internal representation of the cart + /// + private Models.N3DS.Cart _cart; + + #endregion + + #region Constructors + + /// + /// Private constructor + /// + private N3DS() { } + + /// + /// Create a 3DS cart image from a byte array and offset + /// + /// Byte array representing the archive + /// Offset within the array to parse + /// A 3DS cart image wrapper on success, null on failure + public static N3DS Create(byte[] data, int offset) + { + // If the data is invalid + if (data == null) + return null; + + // If the offset is out of bounds + if (offset < 0 || offset >= data.Length) + return null; + + // Create a memory stream and use that + MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); + return Create(dataStream); + } + + /// + /// Create a 3DS cart image from a Stream + /// + /// Stream representing the archive + /// A 3DS cart image wrapper on success, null on failure + public static N3DS Create(Stream data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + var archive = Builders.N3DS.ParseCart(data); + if (archive == null) + return null; + + var wrapper = new N3DS + { + _cart = archive, + _dataSource = DataSource.Stream, + _streamData = data, + }; + return wrapper; + } + + #endregion + + #region Printing + + /// + public override void Print() + { + Console.WriteLine("3DS Cart Information:"); + Console.WriteLine("-------------------------"); + Console.WriteLine(); + + PrintNCSDHeader(); + PrintCardInfoHeader(); + PrintDevelopmentCardInfoHeader(); + PrintPartitions(); + } + + /// + /// Print NCSD header information + /// + private void PrintNCSDHeader() + { + Console.WriteLine(" NCSD Header Information:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" RSA-2048 SHA-256 signature: {BitConverter.ToString(RSA2048Signature).Replace('-', ' ')}"); + Console.WriteLine($" Magic number: {MagicNumber}"); + Console.WriteLine($" Image size in media units: {ImageSizeInMediaUnits}"); + Console.WriteLine($" Media ID: {BitConverter.ToString(MediaId).Replace('-', ' ')}"); + Console.WriteLine($" Partitions filesystem type: {PartitionsFSType}"); + Console.WriteLine($" Partitions crypt type: {BitConverter.ToString(PartitionsCryptType).Replace('-', ' ')}"); + Console.WriteLine(); + + Console.WriteLine($" Partition table:"); + Console.WriteLine(" -------------------------"); + for (int i = 0; i < PartitionsTable.Length; i++) + { + var partitionTableEntry = PartitionsTable[i]; + Console.WriteLine($" Partition table entry {i}"); + Console.WriteLine($" Offset: {partitionTableEntry.Offset}"); + Console.WriteLine($" Length: {partitionTableEntry.Length}"); + } + Console.WriteLine(); + + // If we have a cart image + if (PartitionsFSType == Models.N3DS.FilesystemType.Normal || PartitionsFSType == Models.N3DS.FilesystemType.None) + { + Console.WriteLine($" Exheader SHA-256 hash: {BitConverter.ToString(ExheaderHash).Replace('-', ' ')}"); + Console.WriteLine($" Additional header size: {AdditionalHeaderSize}"); + Console.WriteLine($" Sector zero offset: {SectorZeroOffset}"); + Console.WriteLine($" Partition flags: {BitConverter.ToString(PartitionFlags).Replace('-', ' ')}"); + Console.WriteLine(); + + Console.WriteLine($" Partition ID table:"); + Console.WriteLine(" -------------------------"); + for (int i = 0; i < PartitionIdTable.Length; i++) + { + Console.WriteLine($" Partition {i} ID: {PartitionIdTable[i]}"); + } + Console.WriteLine(); + + Console.WriteLine($" Reserved 1: {BitConverter.ToString(Reserved1).Replace('-', ' ')}"); + Console.WriteLine($" Reserved 2: {BitConverter.ToString(Reserved2).Replace('-', ' ')}"); + Console.WriteLine($" Firmware update byte 1: {FirmUpdateByte1}"); + Console.WriteLine($" Firmware update byte 2: {FirmUpdateByte2}"); + } + + // If we have a firmware image + else if (PartitionsFSType == Models.N3DS.FilesystemType.FIRM) + { + Console.WriteLine($" Unknown: {BitConverter.ToString(Unknown).Replace('-', ' ')}"); + Console.WriteLine($" Encrypted MBR: {BitConverter.ToString(EncryptedMBR).Replace('-', ' ')}"); + } + + Console.WriteLine(); + } + + /// + /// Print card info header information + /// + private void PrintCardInfoHeader() + { + Console.WriteLine(" Card Info Header Information:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" Writable address in media units: {CIH_WritableAddressMediaUnits}"); + Console.WriteLine($" Card info bitmask: {CIH_CardInfoBitmask}"); + Console.WriteLine($" Reserved 1: {BitConverter.ToString(CIH_Reserved1).Replace('-', ' ')}"); + Console.WriteLine($" Filled size of cartridge: {CIH_FilledSize}"); + Console.WriteLine($" Reserved 2: {BitConverter.ToString(CIH_Reserved2).Replace('-', ' ')}"); + Console.WriteLine($" Title version: {CIH_TitleVersion}"); + Console.WriteLine($" Card revision: {CIH_CardRevision}"); + Console.WriteLine($" Reserved 3: {BitConverter.ToString(CIH_Reserved3).Replace('-', ' ')}"); + Console.WriteLine($" Title ID of CVer in included update partition: {BitConverter.ToString(CIH_CVerTitleID).Replace('-', ' ')}"); + Console.WriteLine($" Version number of CVer in included update partition: {CIH_CVerVersionNumber}"); + Console.WriteLine($" Reserved 4: {BitConverter.ToString(CIH_Reserved4).Replace('-', ' ')}"); + Console.WriteLine(); + } + + /// + /// Print development card info header information + /// + private void PrintDevelopmentCardInfoHeader() + { + Console.WriteLine(" Development Card Info Header Information:"); + Console.WriteLine(" -------------------------"); + if (_cart.DevelopmentCardInfoHeader == null) + { + Console.WriteLine(" No development card info header"); + } + else + { + Console.WriteLine(); + Console.WriteLine(" Initial Data:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" Card seed keyY: {BitConverter.ToString(DCIH_ID_CardSeedKeyY).Replace('-', ' ')}"); + Console.WriteLine($" Encrypted card seed: {BitConverter.ToString(DCIH_ID_EncryptedCardSeed).Replace('-', ' ')}"); + Console.WriteLine($" Card seed AES-MAC: {BitConverter.ToString(DCIH_ID_CardSeedAESMAC).Replace('-', ' ')}"); + Console.WriteLine($" Card seed nonce: {BitConverter.ToString(DCIH_ID_CardSeedNonce).Replace('-', ' ')}"); + Console.WriteLine($" Reserved: {BitConverter.ToString(DCIH_ID_Reserved).Replace('-', ' ')}"); + // TODO: Print Backup Header? + Console.WriteLine(); + + Console.WriteLine($" Card device reserved 1: {BitConverter.ToString(DCIH_CardDeviceReserved1).Replace('-', ' ')}"); + Console.WriteLine($" Title key: {BitConverter.ToString(DCIH_TitleKey).Replace('-', ' ')}"); + Console.WriteLine($" Card device reserved 2: {BitConverter.ToString(DCIH_CardDeviceReserved2).Replace('-', ' ')}"); + Console.WriteLine(); + + Console.WriteLine(" Test Data:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" Signature: {BitConverter.ToString(DCIH_TD_Signature).Replace('-', ' ')}"); + Console.WriteLine($" Ascending byte sequence: {BitConverter.ToString(DCIH_TD_AscendingByteSequence).Replace('-', ' ')}"); + Console.WriteLine($" Descending byte sequence: {BitConverter.ToString(DCIH_TD_DescendingByteSequence).Replace('-', ' ')}"); + Console.WriteLine($" Filled with 00: {BitConverter.ToString(DCIH_TD_Filled00).Replace('-', ' ')}"); + Console.WriteLine($" Filled with FF: {BitConverter.ToString(DCIH_TD_FilledFF).Replace('-', ' ')}"); + Console.WriteLine($" Filled with 0F: {BitConverter.ToString(DCIH_TD_Filled0F).Replace('-', ' ')}"); + Console.WriteLine($" Filled with F0: {BitConverter.ToString(DCIH_TD_FilledF0).Replace('-', ' ')}"); + Console.WriteLine($" Filled with 55: {BitConverter.ToString(DCIH_TD_Filled55).Replace('-', ' ')}"); + Console.WriteLine($" Filled with AA: {BitConverter.ToString(DCIH_TD_FilledAA).Replace('-', ' ')}"); + Console.WriteLine($" Final byte: {DCIH_TD_FinalByte}"); + } + + Console.WriteLine(); + } + + /// + /// Print NCCH partition header information + /// + private void PrintPartitions() + { + Console.WriteLine(" NCCH Partition Header Information:"); + Console.WriteLine(" -------------------------"); + if (Partitions == null || Partitions.Length == 0) + { + Console.WriteLine(" No NCCH partition headers"); + } + else + { + for (int i = 0; i < Partitions.Length; i++) + { + var partitionHeader = Partitions[i]; + Console.WriteLine($" NCCH Partition Header {i}"); + Console.WriteLine($" RSA-2048 SHA-256 signature: {BitConverter.ToString(partitionHeader.RSA2048Signature).Replace('-', ' ')}"); + Console.WriteLine($" Magic ID: {partitionHeader.MagicID}"); + Console.WriteLine($" Content size in media units: {partitionHeader.ContentSizeInMediaUnits}"); + Console.WriteLine($" Partition ID: {partitionHeader.PartitionId}"); + Console.WriteLine($" Maker code: {partitionHeader.MakerCode}"); + Console.WriteLine($" Version: {partitionHeader.Version}"); + Console.WriteLine($" Verification hash: {partitionHeader.VerificationHash}"); + Console.WriteLine($" Program ID: {BitConverter.ToString(partitionHeader.ProgramId).Replace('-', ' ')}"); + Console.WriteLine($" Reserved 1: {BitConverter.ToString(partitionHeader.Reserved1).Replace('-', ' ')}"); + Console.WriteLine($" Logo region SHA-256 hash: {BitConverter.ToString(partitionHeader.LogoRegionHash).Replace('-', ' ')}"); + Console.WriteLine($" Product code: {BitConverter.ToString(partitionHeader.ProductCode).Replace('-', ' ')}"); + Console.WriteLine($" Extended header SHA-256 hash: {BitConverter.ToString(partitionHeader.ExtendedHeaderHash).Replace('-', ' ')}"); + Console.WriteLine($" Extended header size in bytes: {partitionHeader.ExtendedHeaderSizeInBytes}"); + Console.WriteLine($" Reserved 2: {BitConverter.ToString(partitionHeader.Reserved2).Replace('-', ' ')}"); + Console.WriteLine($" Flags: {partitionHeader.Flags}"); + Console.WriteLine($" Plain region offset, in media units: {partitionHeader.PlainRegionOffsetInMediaUnits}"); + Console.WriteLine($" Plain region size, in media units: {partitionHeader.PlainRegionSizeInMediaUnits}"); + Console.WriteLine($" Logo region offset, in media units: {partitionHeader.LogoRegionOffsetInMediaUnits}"); + Console.WriteLine($" Logo region size, in media units: {partitionHeader.LogoRegionSizeInMediaUnits}"); + Console.WriteLine($" ExeFS offset, in media units: {partitionHeader.ExeFSOffsetInMediaUnits}"); + Console.WriteLine($" ExeFS size, in media units: {partitionHeader.ExeFSSizeInMediaUnits}"); + Console.WriteLine($" ExeFS hash region size, in media units: {partitionHeader.ExeFSHashRegionSizeInMediaUnits}"); + Console.WriteLine($" Reserved 3: {BitConverter.ToString(partitionHeader.Reserved3).Replace('-', ' ')}"); + Console.WriteLine($" RomFS offset, in media units: {partitionHeader.RomFSOffsetInMediaUnits}"); + Console.WriteLine($" RomFS size, in media units: {partitionHeader.RomFSSizeInMediaUnits}"); + Console.WriteLine($" RomFS hash region size, in media units: {partitionHeader.RomFSHashRegionSizeInMediaUnits}"); + Console.WriteLine($" Reserved 4: {BitConverter.ToString(partitionHeader.Reserved4).Replace('-', ' ')}"); + Console.WriteLine($" ExeFS superblock SHA-256 hash: {BitConverter.ToString(partitionHeader.ExeFSSuperblockHash).Replace('-', ' ')}"); + Console.WriteLine($" RomFS superblock SHA-256 hash: {BitConverter.ToString(partitionHeader.RomFSSuperblockHash).Replace('-', ' ')}"); + } + } + Console.WriteLine(); + } + + #endregion + } +} \ No newline at end of file