From a0fafd0496b9bd483468b7f6b45b9cb77b78914c Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 8 Sep 2023 22:48:56 -0400 Subject: [PATCH] Add and convert NDS --- Bytes/Nitro.Deserializer.cs | 28 +++ Streams/Nitro.Deserializer.cs | 364 ++++++++++++++++++++++++++++++++++ Streams/Nitro.Serializer.cs | 16 ++ 3 files changed, 408 insertions(+) create mode 100644 Bytes/Nitro.Deserializer.cs create mode 100644 Streams/Nitro.Deserializer.cs create mode 100644 Streams/Nitro.Serializer.cs diff --git a/Bytes/Nitro.Deserializer.cs b/Bytes/Nitro.Deserializer.cs new file mode 100644 index 00000000..eedc867b --- /dev/null +++ b/Bytes/Nitro.Deserializer.cs @@ -0,0 +1,28 @@ +using System.IO; +using SabreTools.Models.Nitro; + +namespace SabreTools.Serialization.Bytes +{ + public partial class Nitro : 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.Nitro().Deserialize(dataStream); + } + } +} \ No newline at end of file diff --git a/Streams/Nitro.Deserializer.cs b/Streams/Nitro.Deserializer.cs new file mode 100644 index 00000000..4aa1f18e --- /dev/null +++ b/Streams/Nitro.Deserializer.cs @@ -0,0 +1,364 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using SabreTools.IO; +using SabreTools.Models.Nitro; + +namespace SabreTools.Serialization.Streams +{ + public partial class Nitro : 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 Header + + // Try to parse the header + var header = ParseCommonHeader(data); + if (header == null) + return null; + + // Set the cart image header + cart.CommonHeader = header; + + #endregion + + #region Extended DSi Header + + // If we have a DSi-compatible cartridge + if (header.UnitCode == Unitcode.NDSPlusDSi || header.UnitCode == Unitcode.DSi) + { + var extendedDSiHeader = ParseExtendedDSiHeader(data); + if (extendedDSiHeader == null) + return null; + + cart.ExtendedDSiHeader = extendedDSiHeader; + } + + #endregion + + #region Secure Area + + // Try to get the secure area offset + long secureAreaOffset = 0x4000; + if (secureAreaOffset > data.Length) + return null; + + // Seek to the secure area + data.Seek(secureAreaOffset, SeekOrigin.Begin); + + // Read the secure area without processing + cart.SecureArea = data.ReadBytes(0x800); + + #endregion + + #region Name Table + + // Try to get the name table offset + long nameTableOffset = header.FileNameTableOffset; + if (nameTableOffset < 0 || nameTableOffset > data.Length) + return null; + + // Seek to the name table + data.Seek(nameTableOffset, SeekOrigin.Begin); + + // Try to parse the name table + var nameTable = ParseNameTable(data); + if (nameTable == null) + return null; + + // Set the name table + cart.NameTable = nameTable; + + #endregion + + #region File Allocation Table + + // Try to get the file allocation table offset + long fileAllocationTableOffset = header.FileAllocationTableOffset; + if (fileAllocationTableOffset < 0 || fileAllocationTableOffset > data.Length) + return null; + + // Seek to the file allocation table + data.Seek(fileAllocationTableOffset, SeekOrigin.Begin); + + // Create the file allocation table + var fileAllocationTable = new List(); + + // Try to parse the file allocation table + while (data.Position - fileAllocationTableOffset < header.FileAllocationTableLength) + { + var entry = ParseFileAllocationTableEntry(data); + fileAllocationTable.Add(entry); + } + + // Set the file allocation table + cart.FileAllocationTable = fileAllocationTable.ToArray(); + + #endregion + + // TODO: Read and optionally parse out the other areas + // Look for offsets and lengths in the header pieces + + return cart; + } + + /// + /// Parse a Stream into a common header + /// + /// Stream to parse + /// Filled common header on success, null on error + private static CommonHeader ParseCommonHeader(Stream data) + { + // TODO: Use marshalling here instead of building + CommonHeader commonHeader = new CommonHeader(); + + byte[] gameTitle = data.ReadBytes(12); + commonHeader.GameTitle = Encoding.ASCII.GetString(gameTitle).TrimEnd('\0'); + commonHeader.GameCode = data.ReadUInt32(); + byte[] makerCode = data.ReadBytes(2); + commonHeader.MakerCode = Encoding.ASCII.GetString(bytes: makerCode).TrimEnd('\0'); + commonHeader.UnitCode = (Unitcode)data.ReadByteValue(); + commonHeader.EncryptionSeedSelect = data.ReadByteValue(); + commonHeader.DeviceCapacity = data.ReadByteValue(); + commonHeader.Reserved1 = data.ReadBytes(7); + commonHeader.GameRevision = data.ReadUInt16(); + commonHeader.RomVersion = data.ReadByteValue(); + commonHeader.InternalFlags = data.ReadByteValue(); + commonHeader.ARM9RomOffset = data.ReadUInt32(); + commonHeader.ARM9EntryAddress = data.ReadUInt32(); + commonHeader.ARM9LoadAddress = data.ReadUInt32(); + commonHeader.ARM9Size = data.ReadUInt32(); + commonHeader.ARM7RomOffset = data.ReadUInt32(); + commonHeader.ARM7EntryAddress = data.ReadUInt32(); + commonHeader.ARM7LoadAddress = data.ReadUInt32(); + commonHeader.ARM7Size = data.ReadUInt32(); + commonHeader.FileNameTableOffset = data.ReadUInt32(); + commonHeader.FileNameTableLength = data.ReadUInt32(); + commonHeader.FileAllocationTableOffset = data.ReadUInt32(); + commonHeader.FileAllocationTableLength = data.ReadUInt32(); + commonHeader.ARM9OverlayOffset = data.ReadUInt32(); + commonHeader.ARM9OverlayLength = data.ReadUInt32(); + commonHeader.ARM7OverlayOffset = data.ReadUInt32(); + commonHeader.ARM7OverlayLength = data.ReadUInt32(); + commonHeader.NormalCardControlRegisterSettings = data.ReadUInt32(); + commonHeader.SecureCardControlRegisterSettings = data.ReadUInt32(); + commonHeader.IconBannerOffset = data.ReadUInt32(); + commonHeader.SecureAreaCRC = data.ReadUInt16(); + commonHeader.SecureTransferTimeout = data.ReadUInt16(); + commonHeader.ARM9Autoload = data.ReadUInt32(); + commonHeader.ARM7Autoload = data.ReadUInt32(); + commonHeader.SecureDisable = data.ReadBytes(8); + commonHeader.NTRRegionRomSize = data.ReadUInt32(); + commonHeader.HeaderSize = data.ReadUInt32(); + commonHeader.Reserved2 = data.ReadBytes(56); + commonHeader.NintendoLogo = data.ReadBytes(156); + commonHeader.NintendoLogoCRC = data.ReadUInt16(); + commonHeader.HeaderCRC = data.ReadUInt16(); + commonHeader.DebuggerReserved = data.ReadBytes(0x20); + + return commonHeader; + } + + /// + /// Parse a Stream into an extended DSi header + /// + /// Stream to parse + /// Filled extended DSi header on success, null on error + private static ExtendedDSiHeader ParseExtendedDSiHeader(Stream data) + { + // TODO: Use marshalling here instead of building + ExtendedDSiHeader extendedDSiHeader = new ExtendedDSiHeader(); + + extendedDSiHeader.GlobalMBK15Settings = new uint[5]; + for (int i = 0; i < 5; i++) + { + extendedDSiHeader.GlobalMBK15Settings[i] = data.ReadUInt32(); + } + extendedDSiHeader.LocalMBK68SettingsARM9 = new uint[3]; + for (int i = 0; i < 3; i++) + { + extendedDSiHeader.LocalMBK68SettingsARM9[i] = data.ReadUInt32(); + } + extendedDSiHeader.LocalMBK68SettingsARM7 = new uint[3]; + for (int i = 0; i < 3; i++) + { + extendedDSiHeader.LocalMBK68SettingsARM7[i] = data.ReadUInt32(); + } + extendedDSiHeader.GlobalMBK9Setting = data.ReadUInt32(); + extendedDSiHeader.RegionFlags = data.ReadUInt32(); + extendedDSiHeader.AccessControl = data.ReadUInt32(); + extendedDSiHeader.ARM7SCFGEXTMask = data.ReadUInt32(); + extendedDSiHeader.ReservedFlags = data.ReadUInt32(); + extendedDSiHeader.ARM9iRomOffset = data.ReadUInt32(); + extendedDSiHeader.Reserved3 = data.ReadUInt32(); + extendedDSiHeader.ARM9iLoadAddress = data.ReadUInt32(); + extendedDSiHeader.ARM9iSize = data.ReadUInt32(); + extendedDSiHeader.ARM7iRomOffset = data.ReadUInt32(); + extendedDSiHeader.Reserved4 = data.ReadUInt32(); + extendedDSiHeader.ARM7iLoadAddress = data.ReadUInt32(); + extendedDSiHeader.ARM7iSize = data.ReadUInt32(); + extendedDSiHeader.DigestNTRRegionOffset = data.ReadUInt32(); + extendedDSiHeader.DigestNTRRegionLength = data.ReadUInt32(); + extendedDSiHeader.DigestTWLRegionOffset = data.ReadUInt32(); + extendedDSiHeader.DigestTWLRegionLength = data.ReadUInt32(); + extendedDSiHeader.DigestSectorHashtableRegionOffset = data.ReadUInt32(); + extendedDSiHeader.DigestSectorHashtableRegionLength = data.ReadUInt32(); + extendedDSiHeader.DigestBlockHashtableRegionOffset = data.ReadUInt32(); + extendedDSiHeader.DigestBlockHashtableRegionLength = data.ReadUInt32(); + extendedDSiHeader.DigestSectorSize = data.ReadUInt32(); + extendedDSiHeader.DigestBlockSectorCount = data.ReadUInt32(); + extendedDSiHeader.IconBannerSize = data.ReadUInt32(); + extendedDSiHeader.Unknown1 = data.ReadUInt32(); + extendedDSiHeader.ModcryptArea1Offset = data.ReadUInt32(); + extendedDSiHeader.ModcryptArea1Size = data.ReadUInt32(); + extendedDSiHeader.ModcryptArea2Offset = data.ReadUInt32(); + extendedDSiHeader.ModcryptArea2Size = data.ReadUInt32(); + extendedDSiHeader.TitleID = data.ReadBytes(8); + extendedDSiHeader.DSiWarePublicSavSize = data.ReadUInt32(); + extendedDSiHeader.DSiWarePrivateSavSize = data.ReadUInt32(); + extendedDSiHeader.ReservedZero = data.ReadBytes(176); + extendedDSiHeader.Unknown2 = data.ReadBytes(0x10); + extendedDSiHeader.ARM9WithSecureAreaSHA1HMACHash = data.ReadBytes(20); + extendedDSiHeader.ARM7SHA1HMACHash = data.ReadBytes(20); + extendedDSiHeader.DigestMasterSHA1HMACHash = data.ReadBytes(20); + extendedDSiHeader.BannerSHA1HMACHash = data.ReadBytes(20); + extendedDSiHeader.ARM9iDecryptedSHA1HMACHash = data.ReadBytes(20); + extendedDSiHeader.ARM7iDecryptedSHA1HMACHash = data.ReadBytes(20); + extendedDSiHeader.Reserved5 = data.ReadBytes(40); + extendedDSiHeader.ARM9NoSecureAreaSHA1HMACHash = data.ReadBytes(20); + extendedDSiHeader.Reserved6 = data.ReadBytes(2636); + extendedDSiHeader.ReservedAndUnchecked = data.ReadBytes(0x180); + extendedDSiHeader.RSASignature = data.ReadBytes(0x80); + + return extendedDSiHeader; + } + + /// + /// Parse a Stream into a name table + /// + /// Stream to parse + /// Filled name table on success, null on error + private static NameTable ParseNameTable(Stream data) + { + // TODO: Use marshalling here instead of building + NameTable nameTable = new NameTable(); + + // Create a variable-length table + var folderAllocationTable = new List(); + int entryCount = int.MaxValue; + while (entryCount > 0) + { + var entry = ParseFolderAllocationTableEntry(data); + folderAllocationTable.Add(entry); + + // If we have the root entry + if (entryCount == int.MaxValue) + entryCount = (entry.Unknown << 8) | entry.ParentFolderIndex; + + // Decrement the entry count + entryCount--; + } + + // Assign the folder allocation table + nameTable.FolderAllocationTable = folderAllocationTable.ToArray(); + + // Create a variable-length table + var nameList = new List(); + while (true) + { + var entry = ParseNameListEntry(data); + if (entry == null) + break; + + nameList.Add(entry); + } + + // Assign the name list + nameTable.NameList = nameList.ToArray(); + + return nameTable; + } + + /// + /// Parse a Stream into a folder allocation table entry + /// + /// Stream to parse + /// Filled folder allocation table entry on success, null on error + private static FolderAllocationTableEntry ParseFolderAllocationTableEntry(Stream data) + { + // TODO: Use marshalling here instead of building + FolderAllocationTableEntry entry = new FolderAllocationTableEntry(); + + entry.StartOffset = data.ReadUInt32(); + entry.FirstFileIndex = data.ReadUInt16(); + entry.ParentFolderIndex = data.ReadByteValue(); + entry.Unknown = data.ReadByteValue(); + + return entry; + } + + /// + /// Parse a Stream into a name list entry + /// + /// Stream to parse + /// Filled name list entry on success, null on error + private static NameListEntry ParseNameListEntry(Stream data) + { + // TODO: Use marshalling here instead of building + NameListEntry entry = new NameListEntry(); + + byte flagAndSize = data.ReadByteValue(); + if (flagAndSize == 0xFF) + return null; + + entry.Folder = (flagAndSize & 0x80) != 0; + + byte size = (byte)(flagAndSize & ~0x80); + if (size > 0) + { + byte[] name = data.ReadBytes(size); + entry.Name = Encoding.UTF8.GetString(name); + } + + if (entry.Folder) + entry.Index = data.ReadUInt16(); + + return entry; + } + + /// + /// Parse a Stream into a name list entry + /// + /// Stream to parse + /// Filled name list entry on success, null on error + private static FileAllocationTableEntry ParseFileAllocationTableEntry(Stream data) + { + // TODO: Use marshalling here instead of building + FileAllocationTableEntry entry = new FileAllocationTableEntry(); + + entry.StartOffset = data.ReadUInt32(); + entry.EndOffset = data.ReadUInt32(); + + return entry; + } + } +} \ No newline at end of file diff --git a/Streams/Nitro.Serializer.cs b/Streams/Nitro.Serializer.cs new file mode 100644 index 00000000..d67eb3ad --- /dev/null +++ b/Streams/Nitro.Serializer.cs @@ -0,0 +1,16 @@ +using System; +using System.IO; +using SabreTools.Models.Nitro; + +namespace SabreTools.Serialization.Streams +{ + public partial class Nitro : 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