Files
SabreTools.Serialization/SabreTools.Serialization.Readers/Nitro.cs
Matt Nadareski 8f49e190d8 Fix everything
2026-03-24 19:17:25 -04:00

350 lines
14 KiB
C#

using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Data.Models.Nitro;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
#pragma warning disable IDE0017 // Simplify object initialization
namespace SabreTools.Serialization.Readers
{
public class Nitro : BaseBinaryReader<Cart>
{
/// <inheritdoc/>
public override Cart? Deserialize(Stream? data)
{
// If the data is invalid
if (data is null || !data.CanRead)
return null;
try
{
// Cache the current offset
long initialOffset = data.Position;
// Create a new cart image to fill
var cart = new Cart();
#region Header
// Set the cart image header
cart.CommonHeader = ParseCommonHeader(data);
#endregion
#region Extended DSi Header
// If we have a DSi-compatible cartridge
if (cart.CommonHeader.UnitCode == Unitcode.NDSPlusDSi || cart.CommonHeader.UnitCode == Unitcode.DSi)
cart.ExtendedDSiHeader = ParseExtendedDSiHeader(data);
#endregion
#region Secure Area
// Try to get the secure area offset
long secureAreaOffset = initialOffset + 0x4000;
if (secureAreaOffset > data.Length)
return null;
// Seek to the secure area
data.SeekIfPossible(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 = initialOffset + cart.CommonHeader.FileNameTableOffset;
if (nameTableOffset < initialOffset || nameTableOffset > data.Length)
return null;
// Seek to the name table
data.SeekIfPossible(nameTableOffset, SeekOrigin.Begin);
// Set the name table
cart.NameTable = ParseNameTable(data);
#endregion
#region File Allocation Table
// Try to get the file allocation table offset
long fileAllocationTableOffset = initialOffset + cart.CommonHeader.FileAllocationTableOffset;
if (fileAllocationTableOffset < initialOffset || fileAllocationTableOffset > data.Length)
return null;
// Seek to the file allocation table
data.SeekIfPossible(fileAllocationTableOffset, SeekOrigin.Begin);
// Create the file allocation table
var fileAllocationTable = new List<FileAllocationTableEntry>();
// Try to parse the file allocation table
while (data.Position - fileAllocationTableOffset < cart.CommonHeader.FileAllocationTableLength)
{
var entry = ParseFileAllocationTableEntry(data);
fileAllocationTable.Add(entry);
}
// Set the file allocation table
cart.FileAllocationTable = [.. fileAllocationTable];
#endregion
// TODO: Read and optionally parse out the other areas
// Look for offsets and lengths in the header pieces
return cart;
}
catch
{
// Ignore the actual error
return null;
}
}
/// <summary>
/// Parse a Stream into a CommonHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled CommonHeader on success, null on error</returns>
public static CommonHeader ParseCommonHeader(Stream data)
{
var obj = new CommonHeader();
byte[] gameTitle = data.ReadBytes(12);
obj.GameTitle = Encoding.ASCII.GetString(gameTitle).TrimEnd('\0');
obj.GameCode = data.ReadUInt32LittleEndian();
byte[] makerCode = data.ReadBytes(2);
obj.MakerCode = Encoding.ASCII.GetString(makerCode);
obj.UnitCode = (Unitcode)data.ReadByteValue();
obj.EncryptionSeedSelect = data.ReadByteValue();
obj.DeviceCapacity = data.ReadByteValue();
obj.Reserved1 = data.ReadBytes(7);
obj.GameRevision = data.ReadUInt16LittleEndian();
obj.RomVersion = data.ReadByteValue();
obj.InternalFlags = data.ReadByteValue();
obj.ARM9RomOffset = data.ReadUInt32LittleEndian();
obj.ARM9EntryAddress = data.ReadUInt32LittleEndian();
obj.ARM9LoadAddress = data.ReadUInt32LittleEndian();
obj.ARM9Size = data.ReadUInt32LittleEndian();
obj.ARM7RomOffset = data.ReadUInt32LittleEndian();
obj.ARM7EntryAddress = data.ReadUInt32LittleEndian();
obj.ARM7LoadAddress = data.ReadUInt32LittleEndian();
obj.ARM7Size = data.ReadUInt32LittleEndian();
obj.FileNameTableOffset = data.ReadUInt32LittleEndian();
obj.FileNameTableLength = data.ReadUInt32LittleEndian();
obj.FileAllocationTableOffset = data.ReadUInt32LittleEndian();
obj.FileAllocationTableLength = data.ReadUInt32LittleEndian();
obj.ARM9OverlayOffset = data.ReadUInt32LittleEndian();
obj.ARM9OverlayLength = data.ReadUInt32LittleEndian();
obj.ARM7OverlayOffset = data.ReadUInt32LittleEndian();
obj.ARM7OverlayLength = data.ReadUInt32LittleEndian();
obj.NormalCardControlRegisterSettings = data.ReadUInt32LittleEndian();
obj.SecureCardControlRegisterSettings = data.ReadUInt32LittleEndian();
obj.IconBannerOffset = data.ReadUInt32LittleEndian();
obj.SecureAreaCRC = data.ReadUInt16LittleEndian();
obj.SecureTransferTimeout = data.ReadUInt16LittleEndian();
obj.ARM9Autoload = data.ReadUInt32LittleEndian();
obj.ARM7Autoload = data.ReadUInt32LittleEndian();
obj.SecureDisable = data.ReadBytes(8);
obj.NTRRegionRomSize = data.ReadUInt32LittleEndian();
obj.HeaderSize = data.ReadUInt32LittleEndian();
obj.Reserved2 = data.ReadBytes(56);
obj.NintendoLogo = data.ReadBytes(156);
obj.NintendoLogoCRC = data.ReadUInt16LittleEndian();
obj.HeaderCRC = data.ReadUInt16LittleEndian();
obj.DebuggerReserved = data.ReadBytes(0x20);
return obj;
}
/// <summary>
/// Parse a Stream into a ExtendedDSiHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ExtendedDSiHeader on success, null on error</returns>
public static ExtendedDSiHeader ParseExtendedDSiHeader(Stream data)
{
var obj = new ExtendedDSiHeader();
obj.GlobalMBK15Settings = new uint[5];
for (int i = 0; i < 5; i++)
{
obj.GlobalMBK15Settings[i] = data.ReadUInt32LittleEndian();
}
obj.LocalMBK68SettingsARM9 = new uint[3];
for (int i = 0; i < 3; i++)
{
obj.LocalMBK68SettingsARM9[i] = data.ReadUInt32LittleEndian();
}
obj.LocalMBK68SettingsARM7 = new uint[3];
for (int i = 0; i < 3; i++)
{
obj.LocalMBK68SettingsARM7[i] = data.ReadUInt32LittleEndian();
}
obj.GlobalMBK9Setting = data.ReadUInt32LittleEndian();
obj.RegionFlags = data.ReadUInt32LittleEndian();
obj.AccessControl = data.ReadUInt32LittleEndian();
obj.ARM7SCFGEXTMask = data.ReadUInt32LittleEndian();
obj.ReservedFlags = data.ReadUInt32LittleEndian();
obj.ARM9iRomOffset = data.ReadUInt32LittleEndian();
obj.Reserved3 = data.ReadUInt32LittleEndian();
obj.ARM9iLoadAddress = data.ReadUInt32LittleEndian();
obj.ARM9iSize = data.ReadUInt32LittleEndian();
obj.ARM7iRomOffset = data.ReadUInt32LittleEndian();
obj.Reserved4 = data.ReadUInt32LittleEndian();
obj.ARM7iLoadAddress = data.ReadUInt32LittleEndian();
obj.ARM7iSize = data.ReadUInt32LittleEndian();
obj.DigestNTRRegionOffset = data.ReadUInt32LittleEndian();
obj.DigestNTRRegionLength = data.ReadUInt32LittleEndian();
obj.DigestTWLRegionOffset = data.ReadUInt32LittleEndian();
obj.DigestTWLRegionLength = data.ReadUInt32LittleEndian();
obj.DigestSectorHashtableRegionOffset = data.ReadUInt32LittleEndian();
obj.DigestSectorHashtableRegionLength = data.ReadUInt32LittleEndian();
obj.DigestBlockHashtableRegionOffset = data.ReadUInt32LittleEndian();
obj.DigestBlockHashtableRegionLength = data.ReadUInt32LittleEndian();
obj.DigestSectorSize = data.ReadUInt32LittleEndian();
obj.DigestBlockSectorCount = data.ReadUInt32LittleEndian();
obj.IconBannerSize = data.ReadUInt32LittleEndian();
obj.Unknown1 = data.ReadUInt32LittleEndian();
obj.NTRTWLRegionRomSize = data.ReadUInt32LittleEndian();
obj.Unknown2 = data.ReadBytes(12);
obj.ModcryptArea1Offset = data.ReadUInt32LittleEndian();
obj.ModcryptArea1Size = data.ReadUInt32LittleEndian();
obj.ModcryptArea2Offset = data.ReadUInt32LittleEndian();
obj.ModcryptArea2Size = data.ReadUInt32LittleEndian();
obj.TitleID = data.ReadBytes(8);
obj.DSiWarePublicSavSize = data.ReadUInt32LittleEndian();
obj.DSiWarePrivateSavSize = data.ReadUInt32LittleEndian();
obj.ReservedZero = data.ReadBytes(176);
obj.Unknown3 = data.ReadBytes(16);
obj.ARM9WithSecureAreaSHA1HMACHash = data.ReadBytes(20);
obj.ARM7SHA1HMACHash = data.ReadBytes(20);
obj.DigestMasterSHA1HMACHash = data.ReadBytes(20);
obj.BannerSHA1HMACHash = data.ReadBytes(20);
obj.ARM9iDecryptedSHA1HMACHash = data.ReadBytes(20);
obj.ARM7iDecryptedSHA1HMACHash = data.ReadBytes(20);
obj.Reserved5 = data.ReadBytes(40);
obj.ARM9NoSecureAreaSHA1HMACHash = data.ReadBytes(20);
obj.Reserved6 = data.ReadBytes(2636);
obj.ReservedAndUnchecked = data.ReadBytes(0x180);
obj.RSASignature = data.ReadBytes(0x80);
return obj;
}
/// <summary>
/// Parse a Stream into a FileAllocationTableEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled FileAllocationTableEntry on success, null on error</returns>
public static FileAllocationTableEntry ParseFileAllocationTableEntry(Stream data)
{
var obj = new FileAllocationTableEntry();
obj.StartOffset = data.ReadUInt32LittleEndian();
obj.EndOffset = data.ReadUInt32LittleEndian();
return obj;
}
/// <summary>
/// Parse a Stream into a FolderAllocationTableEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled FolderAllocationTableEntry on success, null on error</returns>
public static FolderAllocationTableEntry ParseFolderAllocationTableEntry(Stream data)
{
var obj = new FolderAllocationTableEntry();
obj.StartOffset = data.ReadUInt32LittleEndian();
obj.FirstFileIndex = data.ReadUInt16LittleEndian();
obj.ParentFolderIndex = data.ReadByteValue();
obj.Unknown = data.ReadByteValue();
return obj;
}
/// <summary>
/// Parse a Stream into a NameListEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled NameListEntry on success, null on error</returns>
public static NameListEntry? ParseNameListEntry(Stream data)
{
var 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.ReadUInt16LittleEndian();
return entry;
}
/// <summary>
/// Parse a Stream into a name table
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled name table on success, null on error</returns>
public static NameTable ParseNameTable(Stream data)
{
var nameTable = new NameTable();
// Create a variable-length table
var folderAllocationTable = new List<FolderAllocationTableEntry>();
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];
// Create a variable-length table
var nameList = new List<NameListEntry>();
while (true)
{
var entry = ParseNameListEntry(data);
if (entry is null)
break;
nameList.Add(entry);
}
// Assign the name list
nameTable.NameList = [.. nameList];
return nameTable;
}
}
}