diff --git a/BurnOutSharp.Builders/N3DS.cs b/BurnOutSharp.Builders/N3DS.cs new file mode 100644 index 00000000..0ea02d59 --- /dev/null +++ b/BurnOutSharp.Builders/N3DS.cs @@ -0,0 +1,252 @@ +using System.IO; +using System.Text; +using BurnOutSharp.Models.N3DS; +using BurnOutSharp.Utilities; +using static BurnOutSharp.Models.N3DS.Constants; + +namespace BurnOutSharp.Builders +{ + public class N3DS + { + #region Byte Data + + /// + /// Parse a byte array into a 3DS cart image + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled cart image on success, null on error + public static Cart ParseCart(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 parse that + MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); + return ParseCart(dataStream); + } + + #endregion + + #region Stream Data + + /// + /// Parse a Stream into a 3DS cart image + /// + /// Stream to parse + /// Filled cart image on success, null on error + public static Cart ParseCart(Stream data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new cart image to fill + var cart = new Cart(); + + #region Header + + // Try to parse the header + var header = ParseNCSDHeader(data); + if (header == null) + return null; + + // Set the cart image header + cart.Header = header; + + #endregion + + #region Partitions + + // Create the partition table + cart.Partitions = new NCCHHeader[8]; + + // Iterate and build the partitions + for (int i = 0; i < 8; i++) + { + cart.Partitions[i] = ParseNCCHHeader(data); + } + + #endregion + + return cart; + } + + /// + /// 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) + { + // 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); + if (header.MagicNumber != NCSDMagicNumber) + return null; + + header.ImageSizeInMediaUnits = data.ReadUInt32(); + header.MediaId = data.ReadBytes(8); + header.PartitionsFSType = (FilesystemType)data.ReadUInt64(); + header.PartitionsCryptType = data.ReadBytes(8); + + header.PartitionsTable = new PartitionTableEntry[8]; + for (int i = 0; i < 8; i++) + { + header.PartitionsTable[i] = ParsePartitionTableEntry(data); + } + + if (header.PartitionsFSType == FilesystemType.Normal || header.PartitionsFSType == FilesystemType.None) + { + header.ExheaderHash = data.ReadBytes(0x20); + header.AdditionalHeaderSize = data.ReadUInt32(); + header.SectorZeroOffset = data.ReadUInt32(); + header.PartitionFlags = data.ReadBytes(8); + + header.PartitionIdTable = new byte[8][]; + for (int i = 0; i < 8; i++) + { + header.PartitionIdTable[i] = data.ReadBytes(8); + } + + header.Reserved1 = data.ReadBytes(0x20); + header.Reserved2 = data.ReadBytes(0xE); + 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) + { + header.Unknown = data.ReadBytes(0x5E); + header.EncryptedMBR = data.ReadBytes(0x42); + } + + return header; + } + + /// + /// Parse a Stream into a partition table entry + /// + /// Stream to parse + /// Filled partition table entry on success, null on error + private static PartitionTableEntry ParsePartitionTableEntry(Stream data) + { + // TODO: Use marshalling here instead of building + PartitionTableEntry partitionTableEntry = new PartitionTableEntry(); + + partitionTableEntry.Offset = data.ReadUInt32(); + partitionTableEntry.Length = data.ReadUInt32(); + + return partitionTableEntry; + } + + /// + /// Parse a Stream into an NCCH header + /// + /// Stream to parse + /// Indicates if the signature should be skipped + /// Filled NCCH header on success, null on error + private static NCCHHeader ParseNCCHHeader(Stream data, bool skipSignature = false) + { + // TODO: Use marshalling here instead of building + NCCHHeader header = new NCCHHeader(); + + if (!skipSignature) + header.RSA2048Signature = data.ReadBytes(0x100); + + byte[] magicId = data.ReadBytes(4); + header.MagicID = Encoding.ASCII.GetString(magicId); + if (header.MagicID != NCCHMagicNumber) + return null; + + header.ContentSizeInMediaUnits = data.ReadUInt32(); + header.PartitionId = data.ReadUInt64(); + header.MakerCode = data.ReadUInt16(); + header.Version = data.ReadUInt16(); + header.VerificationHash = data.ReadUInt32(); + header.ProgramId = data.ReadBytes(8); + header.Reserved1 = data.ReadBytes(0x10); + header.LogoRegionHash = data.ReadBytes(0x20); + header.ProductCode = data.ReadBytes(0x10); + header.ExtendedHeaderHash = data.ReadBytes(0x20); + header.ExtendedHeaderSizeInBytes = data.ReadUInt32(); + header.Reserved2 = data.ReadBytes(4); + header.Flags = ParseNCCHHeaderFlags(data); + header.PlainRegionOffsetInMediaUnits = data.ReadUInt32(); + header.PlainRegionSizeInMediaUnits = data.ReadUInt32(); + header.LogoRegionOffsetInMediaUnits = data.ReadUInt32(); + header.LogoRegionSizeInMediaUnits = data.ReadUInt32(); + header.ExeFSOffsetInMediaUnits = data.ReadUInt32(); + header.ExeFSSizeInMediaUnits = data.ReadUInt32(); + header.ExeFSHashRegionSizeInMediaUnits = data.ReadUInt32(); + header.Reserved3 = data.ReadBytes(4); + header.RomFSOffsetInMediaUnits = data.ReadUInt32(); + header.RomFSSizeInMediaUnits = data.ReadUInt32(); + header.RomFSHashRegionSizeInMediaUnits = data.ReadUInt32(); + header.Reserved4 = data.ReadBytes(4); + header.ExeFSSuperblockHash = data.ReadBytes(0x20); + header.RomFSSuperblockHash = data.ReadBytes(0x20); + + return header; + } + + /// + /// Parse a Stream into an NCCH header flags + /// + /// Stream to parse + /// Filled NCCH header flags on success, null on error + private static NCCHHeaderFlags ParseNCCHHeaderFlags(Stream data) + { + // TODO: Use marshalling here instead of building + NCCHHeaderFlags headerFlags = new NCCHHeaderFlags(); + + headerFlags.Reserved0 = data.ReadByteValue(); + headerFlags.Reserved1 = data.ReadByteValue(); + headerFlags.Reserved2 = data.ReadByteValue(); + headerFlags.CryptoMethod = (CryptoMethod)data.ReadByteValue(); + headerFlags.ContentPlatform = (ContentPlatform)data.ReadByteValue(); + headerFlags.MediaPlatformIndex = (ContentType)data.ReadByteValue(); + headerFlags.ContentUnitSize = data.ReadByteValue(); + headerFlags.BitMasks = (BitMasks)data.ReadByteValue(); + + return headerFlags; + } + + #endregion + } +} diff --git a/BurnOutSharp.Models/N3DS/Cart.cs b/BurnOutSharp.Models/N3DS/Cart.cs new file mode 100644 index 00000000..85fe08fe --- /dev/null +++ b/BurnOutSharp.Models/N3DS/Cart.cs @@ -0,0 +1,18 @@ +namespace BurnOutSharp.Models.N3DS +{ + /// + /// Represents a 3DS cart image + /// + public class Cart + { + /// + /// 3DS cart header + /// + public NCSDHeader Header { get; set; } + + /// + /// NCCH partitions + /// + public NCCHHeader[] Partitions { get; set; } + } +} \ No newline at end of file diff --git a/BurnOutSharp.Models/N3DS/NCCHHeader.cs b/BurnOutSharp.Models/N3DS/NCCHHeader.cs index 34eaaef9..c363398b 100644 --- a/BurnOutSharp.Models/N3DS/NCCHHeader.cs +++ b/BurnOutSharp.Models/N3DS/NCCHHeader.cs @@ -44,7 +44,7 @@ /// /// Program ID /// - public ulong ProgramId; + public byte[] ProgramId; /// /// Reserved diff --git a/BurnOutSharp.Models/N3DS/NCSDHeader.cs b/BurnOutSharp.Models/N3DS/NCSDHeader.cs index 7b718b7f..87018d07 100644 --- a/BurnOutSharp.Models/N3DS/NCSDHeader.cs +++ b/BurnOutSharp.Models/N3DS/NCSDHeader.cs @@ -18,6 +18,11 @@ /// public byte[] RSA2048Signature; + /// + /// Magic Number 'NCSD' + /// + public string MagicNumber; + /// /// Size of the NCSD image, in media units (1 media unit = 0x200 bytes) ///