diff --git a/Bytes/CIA.Deserializer.cs b/Bytes/CIA.Deserializer.cs new file mode 100644 index 00000000..0d493afe --- /dev/null +++ b/Bytes/CIA.Deserializer.cs @@ -0,0 +1,27 @@ +using System.IO; + +namespace SabreTools.Serialization.Bytes +{ + public partial class CIA : IByteSerializer + { + /// +#if NET48 + public Models.N3DS.CIA Deserialize(byte[] data, int offset) +#else + public Models.N3DS.CIA? Deserialize(byte[]? data, int offset) +#endif + { + // 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 new Streams.CIA().Deserialize(dataStream); + } + } +} \ No newline at end of file diff --git a/Bytes/N3DS.Deserializer.cs b/Bytes/N3DS.Deserializer.cs new file mode 100644 index 00000000..ebaab835 --- /dev/null +++ b/Bytes/N3DS.Deserializer.cs @@ -0,0 +1,28 @@ +using System.IO; +using SabreTools.Models.N3DS; + +namespace SabreTools.Serialization.Bytes +{ + public partial class N3DS : IByteSerializer + { + /// +#if NET48 + public Cart Deserialize(byte[] data, int offset) +#else + public Cart? Deserialize(byte[]? data, int offset) +#endif + { + // 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 new Streams.N3DS().Deserialize(dataStream); + } + } +} \ No newline at end of file diff --git a/Streams/CIA.Deserializer.cs b/Streams/CIA.Deserializer.cs new file mode 100644 index 00000000..e3d1949e --- /dev/null +++ b/Streams/CIA.Deserializer.cs @@ -0,0 +1,497 @@ +using System; +using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.N3DS; + +namespace SabreTools.Serialization.Streams +{ + public partial class CIA : IStreamSerializer + { + /// +#if NET48 + public Models.N3DS.CIA Deserialize(Stream data) +#else + public Models.N3DS.CIA? Deserialize(Stream? data) +#endif + { + // 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 CIA archive to fill + var cia = new Models.N3DS.CIA(); + + #region CIA Header + + // Try to parse the header + var header = ParseCIAHeader(data); + if (header == null) + return null; + + // Set the CIA archive header + cia.Header = header; + + #endregion + + // Align to 64-byte boundary, if needed + while (data.Position < data.Length - 1 && data.Position % 64 != 0) + { + _ = data.ReadByteValue(); + } + + #region Certificate Chain + + // Create the certificate chain + cia.CertificateChain = new Certificate[3]; + + // Try to parse the certificates + for (int i = 0; i < 3; i++) + { + var certificate = ParseCertificate(data); + if (certificate == null) + return null; + + cia.CertificateChain[i] = certificate; + } + + #endregion + + // Align to 64-byte boundary, if needed + while (data.Position < data.Length - 1 && data.Position % 64 != 0) + { + _ = data.ReadByteValue(); + } + + #region Ticket + + // Try to parse the ticket + var ticket = ParseTicket(data); + if (ticket == null) + return null; + + // Set the ticket + cia.Ticket = ticket; + + #endregion + + // Align to 64-byte boundary, if needed + while (data.Position < data.Length - 1 && data.Position % 64 != 0) + { + _ = data.ReadByteValue(); + } + + #region Title Metadata + + // Try to parse the title metadata + var titleMetadata = ParseTitleMetadata(data); + if (titleMetadata == null) + return null; + + // Set the title metadata + cia.TMDFileData = titleMetadata; + + #endregion + + // Align to 64-byte boundary, if needed + while (data.Position < data.Length - 1 && data.Position % 64 != 0) + { + _ = data.ReadByteValue(); + } + + #region Content File Data + + // Create the partition table + cia.Partitions = new NCCHHeader[8]; + + // Iterate and build the partitions + for (int i = 0; i < 8; i++) + { + cia.Partitions[i] = N3DS.ParseNCCHHeader(data); + } + + #endregion + + // Align to 64-byte boundary, if needed + while (data.Position < data.Length - 1 && data.Position % 64 != 0) + { + _ = data.ReadByteValue(); + } + + #region Meta Data + + // If we have a meta data + if (header.MetaSize > 0) + { + // Try to parse the meta + var meta = ParseMetaData(data); + if (meta == null) + return null; + + // Set the meta + cia.MetaData = meta; + } + + #endregion + + return cia; + } + + /// + /// Parse a Stream into a CIA header + /// + /// Stream to parse + /// Filled CIA header on success, null on error + private static CIAHeader ParseCIAHeader(Stream data) + { + // TODO: Use marshalling here instead of building + CIAHeader ciaHeader = new CIAHeader(); + + ciaHeader.HeaderSize = data.ReadUInt32(); + ciaHeader.Type = data.ReadUInt16(); + ciaHeader.Version = data.ReadUInt16(); + ciaHeader.CertificateChainSize = data.ReadUInt32(); + ciaHeader.TicketSize = data.ReadUInt32(); + ciaHeader.TMDFileSize = data.ReadUInt32(); + ciaHeader.MetaSize = data.ReadUInt32(); + ciaHeader.ContentSize = data.ReadUInt64(); + ciaHeader.ContentIndex = data.ReadBytes(0x2000); + + return ciaHeader; + } + + /// + /// Parse a Stream into a certificate + /// + /// Stream to parse + /// Filled certificate on success, null on error + private static Certificate ParseCertificate(Stream data) + { + // TODO: Use marshalling here instead of building + Certificate certificate = new Certificate(); + + certificate.SignatureType = (SignatureType)data.ReadUInt32(); + switch (certificate.SignatureType) + { + case SignatureType.RSA_4096_SHA1: + certificate.SignatureSize = 0x200; + certificate.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA1: + certificate.SignatureSize = 0x100; + certificate.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA1: + certificate.SignatureSize = 0x3C; + certificate.PaddingSize = 0x40; + break; + case SignatureType.RSA_4096_SHA256: + certificate.SignatureSize = 0x200; + certificate.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA256: + certificate.SignatureSize = 0x100; + certificate.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA256: + certificate.SignatureSize = 0x3C; + certificate.PaddingSize = 0x40; + break; + default: + return null; + } + + certificate.Signature = data.ReadBytes(certificate.SignatureSize); + certificate.Padding = data.ReadBytes(certificate.PaddingSize); + byte[] issuer = data.ReadBytes(0x40); + certificate.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0'); + certificate.KeyType = (PublicKeyType)data.ReadUInt32(); + byte[] name = data.ReadBytes(0x40); + certificate.Name = Encoding.ASCII.GetString(name).TrimEnd('\0'); + certificate.ExpirationTime = data.ReadUInt32(); + + switch (certificate.KeyType) + { + case PublicKeyType.RSA_4096: + certificate.RSAModulus = data.ReadBytes(0x200); + certificate.RSAPublicExponent = data.ReadUInt32(); + certificate.RSAPadding = data.ReadBytes(0x34); + break; + case PublicKeyType.RSA_2048: + certificate.RSAModulus = data.ReadBytes(0x100); + certificate.RSAPublicExponent = data.ReadUInt32(); + certificate.RSAPadding = data.ReadBytes(0x34); + break; + case PublicKeyType.EllipticCurve: + certificate.ECCPublicKey = data.ReadBytes(0x3C); + certificate.ECCPadding = data.ReadBytes(0x3C); + break; + default: + return null; + } + + return certificate; + } + + /// + /// Parse a Stream into a ticket + /// + /// Stream to parse + /// Indicates if the ticket is from CDN + /// Filled ticket on success, null on error + private static Ticket ParseTicket(Stream data, bool fromCdn = false) + { + // TODO: Use marshalling here instead of building + Ticket ticket = new Ticket(); + + ticket.SignatureType = (SignatureType)data.ReadUInt32(); + switch (ticket.SignatureType) + { + case SignatureType.RSA_4096_SHA1: + ticket.SignatureSize = 0x200; + ticket.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA1: + ticket.SignatureSize = 0x100; + ticket.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA1: + ticket.SignatureSize = 0x3C; + ticket.PaddingSize = 0x40; + break; + case SignatureType.RSA_4096_SHA256: + ticket.SignatureSize = 0x200; + ticket.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA256: + ticket.SignatureSize = 0x100; + ticket.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA256: + ticket.SignatureSize = 0x3C; + ticket.PaddingSize = 0x40; + break; + default: + return null; + } + + ticket.Signature = data.ReadBytes(ticket.SignatureSize); + ticket.Padding = data.ReadBytes(ticket.PaddingSize); + byte[] issuer = data.ReadBytes(0x40); + ticket.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0'); + ticket.ECCPublicKey = data.ReadBytes(0x3C); + ticket.Version = data.ReadByteValue(); + ticket.CaCrlVersion = data.ReadByteValue(); + ticket.SignerCrlVersion = data.ReadByteValue(); + ticket.TitleKey = data.ReadBytes(0x10); + ticket.Reserved1 = data.ReadByteValue(); + ticket.TicketID = data.ReadUInt64(); + ticket.ConsoleID = data.ReadUInt32(); + ticket.TitleID = data.ReadUInt64(); + ticket.Reserved2 = data.ReadBytes(2); + ticket.TicketTitleVersion = data.ReadUInt16(); + ticket.Reserved3 = data.ReadBytes(8); + ticket.LicenseType = data.ReadByteValue(); + ticket.CommonKeyYIndex = data.ReadByteValue(); + ticket.Reserved4 = data.ReadBytes(0x2A); + ticket.eShopAccountID = data.ReadUInt32(); + ticket.Reserved5 = data.ReadByteValue(); + ticket.Audit = data.ReadByteValue(); + ticket.Reserved6 = data.ReadBytes(0x42); + ticket.Limits = new uint[0x10]; + for (int i = 0; i < ticket.Limits.Length; i++) + { + ticket.Limits[i] = data.ReadUInt32(); + } + + // Seek to the content index size + data.Seek(4, SeekOrigin.Current); + + // Read the size (big-endian) + byte[] contentIndexSize = data.ReadBytes(4); + Array.Reverse(contentIndexSize); + ticket.ContentIndexSize = BitConverter.ToUInt32(contentIndexSize, 0); + + // Seek back to the start of the content index + data.Seek(-8, SeekOrigin.Current); + + ticket.ContentIndex = data.ReadBytes((int)ticket.ContentIndexSize); + + // Certificates only exist in standalone CETK files + if (fromCdn) + { + ticket.CertificateChain = new Certificate[2]; + for (int i = 0; i < 2; i++) + { + var certificate = ParseCertificate(data); + if (certificate == null) + return null; + + ticket.CertificateChain[i] = certificate; + } + } + + return ticket; + } + + /// + /// Parse a Stream into a title metadata + /// + /// Stream to parse + /// Indicates if the ticket is from CDN + /// Filled title metadata on success, null on error + private static TitleMetadata ParseTitleMetadata(Stream data, bool fromCdn = false) + { + // TODO: Use marshalling here instead of building + TitleMetadata titleMetadata = new TitleMetadata(); + + titleMetadata.SignatureType = (SignatureType)data.ReadUInt32(); + switch (titleMetadata.SignatureType) + { + case SignatureType.RSA_4096_SHA1: + titleMetadata.SignatureSize = 0x200; + titleMetadata.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA1: + titleMetadata.SignatureSize = 0x100; + titleMetadata.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA1: + titleMetadata.SignatureSize = 0x3C; + titleMetadata.PaddingSize = 0x40; + break; + case SignatureType.RSA_4096_SHA256: + titleMetadata.SignatureSize = 0x200; + titleMetadata.PaddingSize = 0x3C; + break; + case SignatureType.RSA_2048_SHA256: + titleMetadata.SignatureSize = 0x100; + titleMetadata.PaddingSize = 0x3C; + break; + case SignatureType.ECDSA_SHA256: + titleMetadata.SignatureSize = 0x3C; + titleMetadata.PaddingSize = 0x40; + break; + default: + return null; + } + + titleMetadata.Signature = data.ReadBytes(titleMetadata.SignatureSize); + titleMetadata.Padding1 = data.ReadBytes(titleMetadata.PaddingSize); + byte[] issuer = data.ReadBytes(0x40); + titleMetadata.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0'); + titleMetadata.Version = data.ReadByteValue(); + titleMetadata.CaCrlVersion = data.ReadByteValue(); + titleMetadata.SignerCrlVersion = data.ReadByteValue(); + titleMetadata.Reserved1 = data.ReadByteValue(); + titleMetadata.SystemVersion = data.ReadUInt64(); + titleMetadata.TitleID = data.ReadUInt64(); + titleMetadata.TitleType = data.ReadUInt32(); + titleMetadata.GroupID = data.ReadUInt16(); + titleMetadata.SaveDataSize = data.ReadUInt32(); + titleMetadata.SRLPrivateSaveDataSize = data.ReadUInt32(); + titleMetadata.Reserved2 = data.ReadBytes(4); + titleMetadata.SRLFlag = data.ReadByteValue(); + titleMetadata.Reserved3 = data.ReadBytes(0x31); + titleMetadata.AccessRights = data.ReadUInt32(); + titleMetadata.TitleVersion = data.ReadUInt16(); + + // Read the content count (big-endian) + byte[] contentCount = data.ReadBytes(2); + Array.Reverse(contentCount); + titleMetadata.ContentCount = BitConverter.ToUInt16(contentCount, 0); + + titleMetadata.BootContent = data.ReadUInt16(); + titleMetadata.Padding2 = data.ReadBytes(2); + titleMetadata.SHA256HashContentInfoRecords = data.ReadBytes(0x20); + titleMetadata.ContentInfoRecords = new ContentInfoRecord[64]; + for (int i = 0; i < 64; i++) + { + titleMetadata.ContentInfoRecords[i] = ParseContentInfoRecord(data); + } + titleMetadata.ContentChunkRecords = new ContentChunkRecord[titleMetadata.ContentCount]; + for (int i = 0; i < titleMetadata.ContentCount; i++) + { + titleMetadata.ContentChunkRecords[i] = ParseContentChunkRecord(data); + } + + // Certificates only exist in standalone TMD files + if (fromCdn) + { + titleMetadata.CertificateChain = new Certificate[2]; + for (int i = 0; i < 2; i++) + { + var certificate = ParseCertificate(data); + if (certificate == null) + return null; + + titleMetadata.CertificateChain[i] = certificate; + } + } + + return titleMetadata; + } + + /// + /// Parse a Stream into a content info record + /// + /// Stream to parse + /// Filled content info record on success, null on error + private static ContentInfoRecord ParseContentInfoRecord(Stream data) + { + // TODO: Use marshalling here instead of building + ContentInfoRecord contentInfoRecord = new ContentInfoRecord(); + + contentInfoRecord.ContentIndexOffset = data.ReadUInt16(); + contentInfoRecord.ContentCommandCount = data.ReadUInt16(); + contentInfoRecord.UnhashedContentRecordsSHA256Hash = data.ReadBytes(0x20); + + return contentInfoRecord; + } + + /// + /// Parse a Stream into a content chunk record + /// + /// Stream to parse + /// Filled content chunk record on success, null on error + private static ContentChunkRecord ParseContentChunkRecord(Stream data) + { + // TODO: Use marshalling here instead of building + ContentChunkRecord contentChunkRecord = new ContentChunkRecord(); + + contentChunkRecord.ContentId = data.ReadUInt32(); + contentChunkRecord.ContentIndex = (ContentIndex)data.ReadUInt16(); + contentChunkRecord.ContentType = (TMDContentType)data.ReadUInt16(); + contentChunkRecord.ContentSize = data.ReadUInt64(); + contentChunkRecord.SHA256Hash = data.ReadBytes(0x20); + + return contentChunkRecord; + } + + /// + /// Parse a Stream into a meta data + /// + /// Stream to parse + /// Filled meta data on success, null on error + private static MetaData ParseMetaData(Stream data) + { + // TODO: Use marshalling here instead of building + MetaData metaData = new MetaData(); + + metaData.TitleIDDependencyList = data.ReadBytes(0x180); + metaData.Reserved1 = data.ReadBytes(0x180); + metaData.CoreVersion = data.ReadUInt32(); + metaData.Reserved2 = data.ReadBytes(0xFC); + metaData.IconData = data.ReadBytes(0x36C0); + + return metaData; + } + } +} \ No newline at end of file diff --git a/Streams/CIA.Serializer.cs b/Streams/CIA.Serializer.cs new file mode 100644 index 00000000..67578b91 --- /dev/null +++ b/Streams/CIA.Serializer.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; + +namespace SabreTools.Serialization.Streams +{ + public partial class CIA : IStreamSerializer + { + /// +#if NET48 + public Stream Serialize(Models.N3DS.CIA obj) => throw new NotImplementedException(); +#else + public Stream? Serialize(Models.N3DS.CIA? obj) => throw new NotImplementedException(); +#endif + } +} \ No newline at end of file diff --git a/Streams/N3DS.Deserializer.cs b/Streams/N3DS.Deserializer.cs new file mode 100644 index 00000000..e18c004e --- /dev/null +++ b/Streams/N3DS.Deserializer.cs @@ -0,0 +1,687 @@ +using System; +using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.N3DS; +using static SabreTools.Models.N3DS.Constants; + +namespace SabreTools.Serialization.Streams +{ + public partial class N3DS : IStreamSerializer + { + /// +#if NET48 + public Cart Deserialize(Stream data) +#else + public Cart? Deserialize(Stream? data) +#endif + { + // 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 NCSD 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 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 + + // 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 + cart.Partitions = new NCCHHeader[8]; + + // Iterate and build the partitions + for (int i = 0; i < 8; i++) + { + cart.Partitions[i] = ParseNCCHHeader(data); + } + + #endregion + + // Cache the media unit size for further use + long mediaUnitSize = (uint)(0x200 * Math.Pow(2, header.PartitionFlags[(int)NCSDFlags.MediaUnitSize])); + + #region Extended Headers + + // Create the extended header table + cart.ExtendedHeaders = new NCCHExtendedHeader[8]; + + // Iterate and build the extended headers + for (int i = 0; i < 8; i++) + { + // If we have an encrypted or invalid partition + if (cart.Partitions[i].MagicID != NCCHMagicNumber) + continue; + + // Get the extended header offset + long offset = (cart.Header.PartitionsTable[i].Offset * mediaUnitSize) + 0x200; + if (offset < 0 || offset >= data.Length) + continue; + + // Seek to the extended header + data.Seek(offset, SeekOrigin.Begin); + + // Parse the extended header + cart.ExtendedHeaders[i] = ParseNCCHExtendedHeader(data); + } + + #endregion + + #region ExeFS Headers + + // Create the ExeFS header table + cart.ExeFSHeaders = new ExeFSHeader[8]; + + // Iterate and build the ExeFS headers + for (int i = 0; i < 8; i++) + { + // If we have an encrypted or invalid partition + if (cart.Partitions[i].MagicID != NCCHMagicNumber) + continue; + + // Get the ExeFS header offset + long offset = (cart.Header.PartitionsTable[i].Offset + cart.Partitions[i].ExeFSOffsetInMediaUnits) * mediaUnitSize; + if (offset < 0 || offset >= data.Length) + continue; + + // Seek to the ExeFS header + data.Seek(offset, SeekOrigin.Begin); + + // Parse the ExeFS header + cart.ExeFSHeaders[i] = ParseExeFSHeader(data); + } + + #endregion + + #region RomFS Headers + + // Create the RomFS header table + cart.RomFSHeaders = new RomFSHeader[8]; + + // Iterate and build the RomFS headers + for (int i = 0; i < 8; i++) + { + // If we have an encrypted or invalid partition + if (cart.Partitions[i].MagicID != NCCHMagicNumber) + continue; + + // Get the RomFS header offset + long offset = (cart.Header.PartitionsTable[i].Offset + cart.Partitions[i].RomFSOffsetInMediaUnits) * mediaUnitSize; + if (offset < 0 || offset >= data.Length) + continue; + + // Seek to the RomFS header + data.Seek(offset, SeekOrigin.Begin); + + // Parse the RomFS header + cart.RomFSHeaders[i] = ParseRomFSHeader(data); + } + + #endregion + + return cart; + } + + /// + /// Parse a Stream into an NCSD header + /// + /// Stream to parse + /// Filled NCSD header on success, null on error + 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).TrimEnd('\0'); ; + 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 ulong[8]; + for (int i = 0; i < 8; i++) + { + header.PartitionIdTable[i] = data.ReadUInt64(); + } + + header.Reserved1 = data.ReadBytes(0x20); + header.Reserved2 = data.ReadBytes(0x0E); + header.FirmUpdateByte1 = data.ReadByteValue(); + header.FirmUpdateByte2 = data.ReadByteValue(); + } + 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 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 + /// + /// Stream to parse + /// Indicates if the signature should be skipped + /// Filled NCCH header on success, null on error + internal 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).TrimEnd('\0'); + 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); + byte[] productCode = data.ReadBytes(0x10); + header.ProductCode = Encoding.ASCII.GetString(productCode).TrimEnd('\0'); + 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; + } + + /// + /// 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; + } + + /// + /// Parse a Stream into an NCCH extended header + /// + /// Stream to parse + /// Filled NCCH extended header on success, null on error + private static NCCHExtendedHeader ParseNCCHExtendedHeader(Stream data) + { + // TODO: Use marshalling here instead of building + NCCHExtendedHeader extendedHeader = new NCCHExtendedHeader(); + + extendedHeader.SCI = ParseSystemControlInfo(data); + if (extendedHeader.SCI == null) + return null; + + extendedHeader.ACI = ParseAccessControlInfo(data); + if (extendedHeader.ACI == null) + return null; + + extendedHeader.AccessDescSignature = data.ReadBytes(0x100); + extendedHeader.NCCHHDRPublicKey = data.ReadBytes(0x100); + + extendedHeader.ACIForLimitations = ParseAccessControlInfo(data); + if (extendedHeader.ACI == null) + return null; + + return extendedHeader; + } + + /// + /// Parse a Stream into a system control info + /// + /// Stream to parse + /// Filled system control info on success, null on error + private static SystemControlInfo ParseSystemControlInfo(Stream data) + { + // TODO: Use marshalling here instead of building + SystemControlInfo systemControlInfo = new SystemControlInfo(); + + byte[] applicationTitle = data.ReadBytes(8); + systemControlInfo.ApplicationTitle = Encoding.ASCII.GetString(applicationTitle).TrimEnd('\0'); + systemControlInfo.Reserved1 = data.ReadBytes(5); + systemControlInfo.Flag = data.ReadByteValue(); + systemControlInfo.RemasterVersion = data.ReadUInt16(); + systemControlInfo.TextCodeSetInfo = ParseCodeSetInfo(data); + systemControlInfo.StackSize = data.ReadUInt32(); + systemControlInfo.ReadOnlyCodeSetInfo = ParseCodeSetInfo(data); + systemControlInfo.Reserved2 = data.ReadBytes(4); + systemControlInfo.DataCodeSetInfo = ParseCodeSetInfo(data); + systemControlInfo.BSSSize = data.ReadUInt32(); + systemControlInfo.DependencyModuleList = new ulong[48]; + for (int i = 0; i < 48; i++) + { + systemControlInfo.DependencyModuleList[i] = data.ReadUInt64(); + } + systemControlInfo.SystemInfo = ParseSystemInfo(data); + + return systemControlInfo; + } + + /// + /// Parse a Stream into a code set info + /// + /// Stream to parse + /// Filled code set info on success, null on error + private static CodeSetInfo ParseCodeSetInfo(Stream data) + { + // TODO: Use marshalling here instead of building + CodeSetInfo codeSetInfo = new CodeSetInfo(); + + codeSetInfo.Address = data.ReadUInt32(); + codeSetInfo.PhysicalRegionSizeInPages = data.ReadUInt32(); + codeSetInfo.SizeInBytes = data.ReadUInt32(); + + return codeSetInfo; + } + + /// + /// Parse a Stream into a system info + /// + /// Stream to parse + /// Filled system info on success, null on error + private static SystemInfo ParseSystemInfo(Stream data) + { + // TODO: Use marshalling here instead of building + SystemInfo systemInfo = new SystemInfo(); + + systemInfo.SaveDataSize = data.ReadUInt64(); + systemInfo.JumpID = data.ReadUInt64(); + systemInfo.Reserved = data.ReadBytes(0x30); + + return systemInfo; + } + + /// + /// Parse a Stream into an access control info + /// + /// Stream to parse + /// Filled access control info on success, null on error + private static AccessControlInfo ParseAccessControlInfo(Stream data) + { + // TODO: Use marshalling here instead of building + AccessControlInfo accessControlInfo = new AccessControlInfo(); + + accessControlInfo.ARM11LocalSystemCapabilities = ParseARM11LocalSystemCapabilities(data); + accessControlInfo.ARM11KernelCapabilities = ParseARM11KernelCapabilities(data); + accessControlInfo.ARM9AccessControl = ParseARM9AccessControl(data); + + return accessControlInfo; + } + + /// + /// Parse a Stream into an ARM11 local system capabilities + /// + /// Stream to parse + /// Filled ARM11 local system capabilities on success, null on error + private static ARM11LocalSystemCapabilities ParseARM11LocalSystemCapabilities(Stream data) + { + // TODO: Use marshalling here instead of building + ARM11LocalSystemCapabilities arm11LocalSystemCapabilities = new ARM11LocalSystemCapabilities(); + + arm11LocalSystemCapabilities.ProgramID = data.ReadUInt64(); + arm11LocalSystemCapabilities.CoreVersion = data.ReadUInt32(); + arm11LocalSystemCapabilities.Flag1 = (ARM11LSCFlag1)data.ReadByteValue(); + arm11LocalSystemCapabilities.Flag2 = (ARM11LSCFlag2)data.ReadByteValue(); + arm11LocalSystemCapabilities.Flag0 = (ARM11LSCFlag0)data.ReadByteValue(); + arm11LocalSystemCapabilities.Priority = data.ReadByteValue(); + arm11LocalSystemCapabilities.ResourceLimitDescriptors = new ushort[16]; + for (int i = 0; i < 16; i++) + { + arm11LocalSystemCapabilities.ResourceLimitDescriptors[i] = data.ReadUInt16(); + } + arm11LocalSystemCapabilities.StorageInfo = ParseStorageInfo(data); + arm11LocalSystemCapabilities.ServiceAccessControl = new ulong[32]; + for (int i = 0; i < 32; i++) + { + arm11LocalSystemCapabilities.ServiceAccessControl[i] = data.ReadUInt64(); + } + arm11LocalSystemCapabilities.ExtendedServiceAccessControl = new ulong[2]; + for (int i = 0; i < 2; i++) + { + arm11LocalSystemCapabilities.ExtendedServiceAccessControl[i] = data.ReadUInt64(); + } + arm11LocalSystemCapabilities.Reserved = data.ReadBytes(0x0F); + arm11LocalSystemCapabilities.ResourceLimitCategory = (ResourceLimitCategory)data.ReadByteValue(); + + return arm11LocalSystemCapabilities; + } + + /// + /// Parse a Stream into a storage info + /// + /// Stream to parse + /// Filled storage info on success, null on error + private static StorageInfo ParseStorageInfo(Stream data) + { + // TODO: Use marshalling here instead of building + StorageInfo storageInfo = new StorageInfo(); + + storageInfo.ExtdataID = data.ReadUInt64(); + storageInfo.SystemSavedataIDs = data.ReadBytes(8); + storageInfo.StorageAccessibleUniqueIDs = data.ReadBytes(8); + storageInfo.FileSystemAccessInfo = data.ReadBytes(7); + storageInfo.OtherAttributes = (StorageInfoOtherAttributes)data.ReadByteValue(); + + return storageInfo; + } + + /// + /// Parse a Stream into an ARM11 kernel capabilities + /// + /// Stream to parse + /// Filled ARM11 kernel capabilities on success, null on error + private static ARM11KernelCapabilities ParseARM11KernelCapabilities(Stream data) + { + // TODO: Use marshalling here instead of building + ARM11KernelCapabilities arm11KernelCapabilities = new ARM11KernelCapabilities(); + + arm11KernelCapabilities.Descriptors = new uint[28]; + for (int i = 0; i < 28; i++) + { + arm11KernelCapabilities.Descriptors[i] = data.ReadUInt32(); + } + arm11KernelCapabilities.Reserved = data.ReadBytes(0x10); + + return arm11KernelCapabilities; + } + + /// + /// Parse a Stream into an ARM11 access control + /// + /// Stream to parse + /// Filled ARM11 access control on success, null on error + private static ARM9AccessControl ParseARM9AccessControl(Stream data) + { + // TODO: Use marshalling here instead of building + ARM9AccessControl arm9AccessControl = new ARM9AccessControl(); + + arm9AccessControl.Descriptors = data.ReadBytes(15); + arm9AccessControl.DescriptorVersion = data.ReadByteValue(); + + return arm9AccessControl; + } + + /// + /// Parse a Stream into an ExeFS header + /// + /// Stream to parse + /// Filled ExeFS header on success, null on error + private static ExeFSHeader ParseExeFSHeader(Stream data) + { + // TODO: Use marshalling here instead of building + ExeFSHeader exeFSHeader = new ExeFSHeader(); + + exeFSHeader.FileHeaders = new ExeFSFileHeader[10]; + for (int i = 0; i < 10; i++) + { + exeFSHeader.FileHeaders[i] = ParseExeFSFileHeader(data); + } + exeFSHeader.Reserved = data.ReadBytes(0x20); + exeFSHeader.FileHashes = new byte[10][]; + for (int i = 0; i < 10; i++) + { + exeFSHeader.FileHashes[i] = data.ReadBytes(0x20); + } + + return exeFSHeader; + } + + /// + /// Parse a Stream into an ExeFS file header + /// + /// Stream to parse + /// Filled ExeFS file header on success, null on error + private static ExeFSFileHeader ParseExeFSFileHeader(Stream data) + { + // TODO: Use marshalling here instead of building + ExeFSFileHeader exeFSFileHeader = new ExeFSFileHeader(); + + byte[] fileName = data.ReadBytes(8); + exeFSFileHeader.FileName = Encoding.ASCII.GetString(fileName).TrimEnd('\0'); + exeFSFileHeader.FileOffset = data.ReadUInt32(); + exeFSFileHeader.FileSize = data.ReadUInt32(); + + return exeFSFileHeader; + } + + /// + /// Parse a Stream into an RomFS header + /// + /// Stream to parse + /// Filled RomFS header on success, null on error + private static RomFSHeader ParseRomFSHeader(Stream data) + { + // TODO: Use marshalling here instead of building + RomFSHeader romFSHeader = new RomFSHeader(); + + byte[] magicString = data.ReadBytes(4); + romFSHeader.MagicString = Encoding.ASCII.GetString(magicString).TrimEnd('\0'); + if (romFSHeader.MagicString != RomFSMagicNumber) + return null; + + romFSHeader.MagicNumber = data.ReadUInt32(); + if (romFSHeader.MagicNumber != RomFSSecondMagicNumber) + return null; + + romFSHeader.MasterHashSize = data.ReadUInt32(); + romFSHeader.Level1LogicalOffset = data.ReadUInt64(); + romFSHeader.Level1HashdataSize = data.ReadUInt64(); + romFSHeader.Level1BlockSizeLog2 = data.ReadUInt32(); + romFSHeader.Reserved1 = data.ReadBytes(4); + romFSHeader.Level2LogicalOffset = data.ReadUInt64(); + romFSHeader.Level2HashdataSize = data.ReadUInt64(); + romFSHeader.Level2BlockSizeLog2 = data.ReadUInt32(); + romFSHeader.Reserved2 = data.ReadBytes(4); + romFSHeader.Level3LogicalOffset = data.ReadUInt64(); + romFSHeader.Level3HashdataSize = data.ReadUInt64(); + romFSHeader.Level3BlockSizeLog2 = data.ReadUInt32(); + romFSHeader.Reserved3 = data.ReadBytes(4); + romFSHeader.Reserved4 = data.ReadBytes(4); + romFSHeader.OptionalInfoSize = data.ReadUInt32(); + + return romFSHeader; + } + } +} \ No newline at end of file diff --git a/Streams/N3DS.Serializer.cs b/Streams/N3DS.Serializer.cs new file mode 100644 index 00000000..31ea0fcd --- /dev/null +++ b/Streams/N3DS.Serializer.cs @@ -0,0 +1,16 @@ +using System; +using System.IO; +using SabreTools.Models.N3DS; + +namespace SabreTools.Serialization.Streams +{ + public partial class N3DS : IStreamSerializer + { + /// +#if NET48 + public Stream Serialize(Cart obj) => throw new NotImplementedException(); +#else + public Stream? Serialize(Cart? obj) => throw new NotImplementedException(); +#endif + } +} \ No newline at end of file