Files
Matt Nadareski 8f49e190d8 Fix everything
2026-03-24 19:17:25 -04:00

618 lines
24 KiB
C#

using System;
using System.IO;
using System.Text;
using SabreTools.Data.Models.N3DS;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
using static SabreTools.Data.Models.N3DS.Constants;
#pragma warning disable IDE0017 // Simplify object initialization
namespace SabreTools.Serialization.Readers
{
public class N3DS : 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 NCSD Header
// Try to parse the header
var header = ParseNCSDHeader(data);
if (header.MagicNumber != NCSDMagicNumber)
return null;
// Set the cart image header
cart.Header = header;
#endregion
#region Card Info Header
// Set the card info header
cart.CardInfoHeader = ParseCardInfoHeader(data);
#endregion
#region Development Card Info Header
// Set the development card info header
cart.DevelopmentCardInfoHeader = ParseDevelopmentCardInfoHeader(data);
#endregion
// Cache the media unit size for further use
long mediaUnitSize = 0;
if (header.PartitionFlags is not null)
mediaUnitSize = (uint)(0x200 * Math.Pow(2, header.PartitionFlags[(int)NCSDFlags.MediaUnitSize]));
#region Partitions
// Create the tables
cart.Partitions = new NCCHHeader[8];
cart.ExtendedHeaders = new NCCHExtendedHeader[8];
cart.ExeFSHeaders = new ExeFSHeader[8];
cart.RomFSHeaders = new RomFSHeader[8];
// Iterate and build the partitions
for (int i = 0; i < 8; i++)
{
// Find the offset to the partition
long partitionOffset = initialOffset + cart.Header.PartitionsTable[i].Offset;
partitionOffset *= mediaUnitSize;
if (partitionOffset <= initialOffset)
continue;
// Seek to the start of the partition
data.SeekIfPossible(partitionOffset, SeekOrigin.Begin);
// Handle the normal header
var partition = ParseNCCHHeader(data);
if (partition is null || partition.MagicID != NCCHMagicNumber)
continue;
// Set the normal header
cart.Partitions[i] = partition;
// Handle the extended header, if it exists
if (partition.ExtendedHeaderSizeInBytes > 0)
cart.ExtendedHeaders[i] = ParseNCCHExtendedHeader(data);
// Handle the ExeFS, if it exists
if (partition.ExeFSSizeInMediaUnits > 0)
{
long offset = partition.ExeFSOffsetInMediaUnits * mediaUnitSize;
data.SeekIfPossible(partitionOffset + offset, SeekOrigin.Begin);
var exeFsHeader = ParseExeFSHeader(data);
if (exeFsHeader is null)
return null;
cart.ExeFSHeaders[i] = exeFsHeader;
}
// Handle the RomFS, if it exists
if (partition.RomFSSizeInMediaUnits > 0)
{
long offset = partition.RomFSOffsetInMediaUnits * mediaUnitSize;
data.SeekIfPossible(partitionOffset + offset, SeekOrigin.Begin);
var romFsHeader = ParseRomFSHeader(data);
if (romFsHeader.MagicString != RomFSMagicNumber)
continue;
if (romFsHeader.MagicNumber != RomFSSecondMagicNumber)
continue;
cart.RomFSHeaders[i] = romFsHeader;
}
}
#endregion
return cart;
}
catch
{
// Ignore the actual error
return null;
}
}
/// <summary>
/// Parse a Stream into a AccessControlInfo
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled AccessControlInfo on success, null on error</returns>
public static AccessControlInfo ParseAccessControlInfo(Stream data)
{
var obj = new AccessControlInfo();
obj.ARM11LocalSystemCapabilities = ParseARM11LocalSystemCapabilities(data);
obj.ARM11KernelCapabilities = ParseARM11KernelCapabilities(data);
obj.ARM9AccessControl = ParseARM9AccessControl(data);
return obj;
}
/// <summary>
/// Parse a Stream into a ARM9AccessControl
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ARM9AccessControl on success, null on error</returns>
public static ARM9AccessControl ParseARM9AccessControl(Stream data)
{
var obj = new ARM9AccessControl();
obj.Descriptors = new ARM9AccessControlDescriptors[15];
for (int i = 0; i < 15; i++)
{
obj.Descriptors[i] = (ARM9AccessControlDescriptors)data.ReadByteValue();
}
obj.DescriptorVersion = data.ReadByteValue();
return obj;
}
/// <summary>
/// Parse a Stream into a ARM11KernelCapabilities
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ARM11KernelCapabilities on success, null on error</returns>
public static ARM11KernelCapabilities ParseARM11KernelCapabilities(Stream data)
{
var obj = new ARM11KernelCapabilities();
obj.Descriptors = new uint[28];
for (int i = 0; i < 28; i++)
{
obj.Descriptors[i] = data.ReadUInt32LittleEndian();
}
obj.Reserved = data.ReadBytes(0x10);
return obj;
}
/// <summary>
/// Parse a Stream into a ARM11LocalSystemCapabilities
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ARM11LocalSystemCapabilities on success, null on error</returns>
public static ARM11LocalSystemCapabilities ParseARM11LocalSystemCapabilities(Stream data)
{
var obj = new ARM11LocalSystemCapabilities();
obj.ProgramID = data.ReadUInt64LittleEndian();
obj.CoreVersion = data.ReadUInt32LittleEndian();
obj.Flag1 = (ARM11LSCFlag1)data.ReadByteValue();
obj.Flag2 = (ARM11LSCFlag2)data.ReadByteValue();
obj.Flag0 = (ARM11LSCFlag0)data.ReadByteValue();
obj.Priority = data.ReadByteValue();
obj.ResourceLimitDescriptors = new ushort[16];
for (int i = 0; i < 16; i++)
{
obj.ResourceLimitDescriptors[i] = data.ReadUInt16LittleEndian();
}
obj.StorageInfo = ParseStorageInfo(data);
obj.ServiceAccessControl = new ulong[32];
for (int i = 0; i < 32; i++)
{
obj.ServiceAccessControl[i] = data.ReadUInt64LittleEndian();
}
obj.ExtendedServiceAccessControl = new ulong[2];
for (int i = 0; i < 2; i++)
{
obj.ExtendedServiceAccessControl[i] = data.ReadUInt64LittleEndian();
}
obj.Reserved = data.ReadBytes(0x0F);
obj.ResourceLimitCategory = (ResourceLimitCategory)data.ReadByteValue();
return obj;
}
/// <summary>
/// Parse a Stream into a CardInfoHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled CardInfoHeader on success, null on error</returns>
public static CardInfoHeader ParseCardInfoHeader(Stream data)
{
var obj = new CardInfoHeader();
obj.WritableAddressMediaUnits = data.ReadUInt32LittleEndian();
obj.CardInfoBitmask = data.ReadUInt32LittleEndian();
obj.Reserved1 = data.ReadBytes(0xF8);
obj.FilledSize = data.ReadUInt32LittleEndian();
obj.Reserved2 = data.ReadBytes(0x0C);
obj.TitleVersion = data.ReadUInt16LittleEndian();
obj.CardRevision = data.ReadUInt16LittleEndian();
obj.Reserved3 = data.ReadBytes(0x0C);
obj.CVerTitleID = data.ReadBytes(0x08);
obj.CVerVersionNumber = data.ReadUInt16LittleEndian();
obj.Reserved4 = data.ReadBytes(0xCD6);
obj.InitialData = ParseInitialData(data);
return obj;
}
/// <summary>
/// Parse a Stream into a CodeSetInfo
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled CodeSetInfo on success, null on error</returns>
public static CodeSetInfo ParseCodeSetInfo(Stream data)
{
var obj = new CodeSetInfo();
obj.Address = data.ReadUInt32LittleEndian();
obj.PhysicalRegionSizeInPages = data.ReadUInt32LittleEndian();
obj.SizeInBytes = data.ReadUInt32LittleEndian();
return obj;
}
/// <summary>
/// Parse a Stream into a DevelopmentCardInfoHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled DevelopmentCardInfoHeader on success, null on error</returns>
public static DevelopmentCardInfoHeader ParseDevelopmentCardInfoHeader(Stream data)
{
var obj = new DevelopmentCardInfoHeader();
obj.CardDeviceReserved1 = data.ReadBytes(0x200);
obj.TitleKey = data.ReadBytes(0x10);
obj.CardDeviceReserved2 = data.ReadBytes(0x1BF0);
obj.TestData = ParseTestData(data);
return obj;
}
/// <summary>
/// Parse a Stream into a ExeFSFileHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ExeFSFileHeader on success, null on error</returns>
public static ExeFSFileHeader ParseExeFSFileHeader(Stream data)
{
var obj = new ExeFSFileHeader();
byte[] fileName = data.ReadBytes(8);
obj.FileName = Encoding.ASCII.GetString(fileName).TrimEnd('\0');
obj.FileOffset = data.ReadUInt32LittleEndian();
obj.FileSize = data.ReadUInt32LittleEndian();
return obj;
}
/// <summary>
/// Parse a Stream into an ExeFSHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ExeFSHeader on success, null on error</returns>
public static ExeFSHeader ParseExeFSHeader(Stream data)
{
var obj = new ExeFSHeader();
obj.FileHeaders = new ExeFSFileHeader[10];
for (int i = 0; i < 10; i++)
{
obj.FileHeaders[i] = ParseExeFSFileHeader(data);
}
obj.Reserved = data.ReadBytes(0x20);
obj.FileHashes = new byte[10][];
for (int i = 0; i < 10; i++)
{
obj.FileHashes[i] = data.ReadBytes(0x20);
}
return obj;
}
/// <summary>
/// Parse a Stream into initial data
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled initial data on success, null on error</returns>
public static InitialData ParseInitialData(Stream data)
{
var obj = new InitialData();
obj.CardSeedKeyY = data.ReadBytes(0x10);
obj.EncryptedCardSeed = data.ReadBytes(0x10);
obj.CardSeedAESMAC = data.ReadBytes(0x10);
obj.CardSeedNonce = data.ReadBytes(0x0C);
obj.Reserved = data.ReadBytes(0xC4);
obj.BackupHeader = ParseNCCHHeader(data, skipSignature: true);
return obj;
}
/// <summary>
/// Parse a Stream into a NCCHExtendedHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled NCCHExtendedHeader on success, null on error</returns>
public static NCCHExtendedHeader ParseNCCHExtendedHeader(Stream data)
{
var obj = new NCCHExtendedHeader();
obj.SCI = ParseSystemControlInfo(data);
obj.ACI = ParseAccessControlInfo(data);
obj.AccessDescSignature = data.ReadBytes(0x100);
obj.NCCHHDRPublicKey = data.ReadBytes(0x100);
obj.ACIForLimitations = ParseAccessControlInfo(data);
return obj;
}
/// <summary>
/// Parse a Stream into an NCCHHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="skipSignature">Indicates if the signature should be skipped</param>
/// <returns>Filled NCCHHeader on success, null on error</returns>
public static NCCHHeader ParseNCCHHeader(Stream data, bool skipSignature = false)
{
var obj = new NCCHHeader();
if (!skipSignature)
obj.RSA2048Signature = data.ReadBytes(0x100);
byte[] magicId = data.ReadBytes(4);
obj.MagicID = Encoding.ASCII.GetString(magicId).TrimEnd('\0');
obj.ContentSizeInMediaUnits = data.ReadUInt32LittleEndian();
obj.PartitionId = data.ReadUInt64LittleEndian();
obj.MakerCode = data.ReadUInt16LittleEndian();
obj.Version = data.ReadUInt16LittleEndian();
obj.VerificationHash = data.ReadUInt32LittleEndian();
obj.ProgramId = data.ReadBytes(8);
obj.Reserved1 = data.ReadBytes(0x10);
obj.LogoRegionHash = data.ReadBytes(0x20);
byte[] productCode = data.ReadBytes(0x10);
obj.ProductCode = Encoding.ASCII.GetString(productCode).TrimEnd('\0');
obj.ExtendedHeaderHash = data.ReadBytes(0x20);
obj.ExtendedHeaderSizeInBytes = data.ReadUInt32LittleEndian();
obj.Reserved2 = data.ReadUInt32LittleEndian();
obj.Flags = ParseNCCHHeaderFlags(data);
obj.PlainRegionOffsetInMediaUnits = data.ReadUInt32LittleEndian();
obj.PlainRegionSizeInMediaUnits = data.ReadUInt32LittleEndian();
obj.LogoRegionOffsetInMediaUnits = data.ReadUInt32LittleEndian();
obj.LogoRegionSizeInMediaUnits = data.ReadUInt32LittleEndian();
obj.ExeFSOffsetInMediaUnits = data.ReadUInt32LittleEndian();
obj.ExeFSSizeInMediaUnits = data.ReadUInt32LittleEndian();
obj.ExeFSHashRegionSizeInMediaUnits = data.ReadUInt32LittleEndian();
obj.Reserved3 = data.ReadUInt32LittleEndian();
obj.RomFSOffsetInMediaUnits = data.ReadUInt32LittleEndian();
obj.RomFSSizeInMediaUnits = data.ReadUInt32LittleEndian();
obj.RomFSHashRegionSizeInMediaUnits = data.ReadUInt32LittleEndian();
obj.Reserved4 = data.ReadUInt32LittleEndian();
obj.ExeFSSuperblockHash = data.ReadBytes(0x20);
obj.RomFSSuperblockHash = data.ReadBytes(0x20);
return obj;
}
/// <summary>
/// Parse a Stream into an NCCHHeaderFlags
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled NCCHHeaderFlags on success, null on error</returns>
public static NCCHHeaderFlags ParseNCCHHeaderFlags(Stream data)
{
var obj = new NCCHHeaderFlags();
obj.Reserved0 = data.ReadByteValue();
obj.Reserved1 = data.ReadByteValue();
obj.Reserved2 = data.ReadByteValue();
obj.CryptoMethod = (CryptoMethod)data.ReadByteValue();
obj.ContentPlatform = (ContentPlatform)data.ReadByteValue();
obj.MediaPlatformIndex = (ContentType)data.ReadByteValue();
obj.ContentUnitSize = data.ReadByteValue();
obj.BitMasks = (BitMasks)data.ReadByteValue();
return obj;
}
/// <summary>
/// Parse a Stream into an NCSDHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled NCSDHeader on success, null on error</returns>
public static NCSDHeader ParseNCSDHeader(Stream data)
{
var obj = new NCSDHeader();
obj.RSA2048Signature = data.ReadBytes(0x100);
byte[] magicNumber = data.ReadBytes(4);
obj.MagicNumber = Encoding.ASCII.GetString(magicNumber).TrimEnd('\0');
obj.ImageSizeInMediaUnits = data.ReadUInt32LittleEndian();
obj.MediaId = data.ReadBytes(8);
obj.PartitionsFSType = (FilesystemType)data.ReadUInt64LittleEndian();
obj.PartitionsCryptType = data.ReadBytes(8);
obj.PartitionsTable = new PartitionTableEntry[8];
for (int i = 0; i < 8; i++)
{
obj.PartitionsTable[i] = ParsePartitionTableEntry(data);
}
if (obj.PartitionsFSType == FilesystemType.Normal || obj.PartitionsFSType == FilesystemType.None)
{
obj.ExheaderHash = data.ReadBytes(0x20);
obj.AdditionalHeaderSize = data.ReadUInt32LittleEndian();
obj.SectorZeroOffset = data.ReadUInt32LittleEndian();
obj.PartitionFlags = data.ReadBytes(8);
obj.PartitionIdTable = new ulong[8];
for (int i = 0; i < 8; i++)
{
obj.PartitionIdTable[i] = data.ReadUInt64LittleEndian();
}
obj.Reserved1 = data.ReadBytes(0x20);
obj.Reserved2 = data.ReadBytes(0x0E);
obj.FirmUpdateByte1 = data.ReadByteValue();
obj.FirmUpdateByte2 = data.ReadByteValue();
}
else if (obj.PartitionsFSType == FilesystemType.FIRM)
{
obj.Unknown = data.ReadBytes(0x5E);
obj.EncryptedMBR = data.ReadBytes(0x42);
}
return obj;
}
/// <summary>
/// Parse a Stream into an PartitionTableEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled PartitionTableEntry on success, null on error</returns>
public static PartitionTableEntry ParsePartitionTableEntry(Stream data)
{
var obj = new PartitionTableEntry();
obj.Offset = data.ReadUInt32LittleEndian();
obj.Length = data.ReadUInt32LittleEndian();
return obj;
}
/// <summary>
/// Parse a Stream into an RomFSHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled RomFSHeader on success, null on error</returns>
public static RomFSHeader ParseRomFSHeader(Stream data)
{
var obj = new RomFSHeader();
byte[] magicString = data.ReadBytes(4);
obj.MagicString = Encoding.ASCII.GetString(magicString);
obj.MagicNumber = data.ReadUInt32LittleEndian();
obj.MasterHashSize = data.ReadUInt32LittleEndian();
obj.Level1LogicalOffset = data.ReadUInt64LittleEndian();
obj.Level1HashdataSize = data.ReadUInt64LittleEndian();
obj.Level1BlockSizeLog2 = data.ReadUInt32LittleEndian();
obj.Reserved1 = data.ReadUInt32LittleEndian();
obj.Level2LogicalOffset = data.ReadUInt64LittleEndian();
obj.Level2HashdataSize = data.ReadUInt64LittleEndian();
obj.Level2BlockSizeLog2 = data.ReadUInt32LittleEndian();
obj.Reserved2 = data.ReadUInt32LittleEndian();
obj.Level3LogicalOffset = data.ReadUInt64LittleEndian();
obj.Level3HashdataSize = data.ReadUInt64LittleEndian();
obj.Level3BlockSizeLog2 = data.ReadUInt32LittleEndian();
obj.Reserved3 = data.ReadUInt32LittleEndian();
obj.Reserved4 = data.ReadUInt32LittleEndian();
obj.OptionalInfoSize = data.ReadUInt32LittleEndian();
return obj;
}
/// <summary>
/// Parse a Stream into an StorageInfo
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled StorageInfo on success, null on error</returns>
public static StorageInfo ParseStorageInfo(Stream data)
{
var obj = new StorageInfo();
obj.ExtdataID = data.ReadUInt64LittleEndian();
obj.SystemSavedataIDs = data.ReadBytes(8);
obj.StorageAccessibleUniqueIDs = data.ReadBytes(8);
obj.FileSystemAccessInfo = data.ReadBytes(7);
obj.OtherAttributes = (StorageInfoOtherAttributes)data.ReadByteValue();
return obj;
}
/// <summary>
/// Parse a Stream into an SystemControlInfo
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled SystemControlInfo on success, null on error</returns>
public static SystemControlInfo ParseSystemControlInfo(Stream data)
{
var obj = new SystemControlInfo();
byte[] applicationTitle = data.ReadBytes(8);
obj.ApplicationTitle = Encoding.ASCII.GetString(applicationTitle).TrimEnd('\0');
obj.Reserved1 = data.ReadBytes(5);
obj.Flag = data.ReadByteValue();
obj.RemasterVersion = data.ReadUInt16LittleEndian();
obj.TextCodeSetInfo = ParseCodeSetInfo(data);
obj.StackSize = data.ReadUInt32LittleEndian();
obj.ReadOnlyCodeSetInfo = ParseCodeSetInfo(data);
obj.Reserved2 = data.ReadUInt32LittleEndian();
obj.DataCodeSetInfo = ParseCodeSetInfo(data);
obj.BSSSize = data.ReadUInt32LittleEndian();
obj.DependencyModuleList = new ulong[48];
for (int i = 0; i < 48; i++)
{
obj.DependencyModuleList[i] = data.ReadUInt64LittleEndian();
}
obj.SystemInfo = ParseSystemInfo(data);
return obj;
}
/// <summary>
/// Parse a Stream into an SystemInfo
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled SystemInfo on success, null on error</returns>
public static SystemInfo ParseSystemInfo(Stream data)
{
var obj = new SystemInfo();
obj.SaveDataSize = data.ReadUInt64LittleEndian();
obj.JumpID = data.ReadUInt64LittleEndian();
obj.Reserved = data.ReadBytes(0x30);
return obj;
}
/// <summary>
/// Parse a Stream into an TestData
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled TestData on success, null on error</returns>
public static TestData ParseTestData(Stream data)
{
var obj = new TestData();
obj.Signature = data.ReadBytes(8);
obj.AscendingByteSequence = data.ReadBytes(0x1F8);
obj.DescendingByteSequence = data.ReadBytes(0x200);
obj.Filled00 = data.ReadBytes(0x200);
obj.FilledFF = data.ReadBytes(0x200);
obj.Filled0F = data.ReadBytes(0x200);
obj.FilledF0 = data.ReadBytes(0x200);
obj.Filled55 = data.ReadBytes(0x200);
obj.FilledAA = data.ReadBytes(0x1FF);
obj.FinalByte = data.ReadByteValue();
return obj;
}
}
}