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