Files
Matt Nadareski 7689c6dd07 Libraries
This change looks dramatic, but it's just separating out the already-split namespaces into separate top-level folders. In theory, every single one could be built into their own Nuget package. `SabreTools.Serialization` still builds the normal Nuget package that is used by all other projects and includes all namespaces.
2026-03-21 16:26:56 -04:00

599 lines
19 KiB
C#

using System;
using System.IO;
using SabreTools.Data.Models.N3DS;
using static SabreTools.Data.Models.N3DS.Constants;
namespace SabreTools.Wrappers
{
public partial class N3DS : WrapperBase<Cart>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "Nintendo 3DS Cart Image";
#endregion
#region Extension Properties
/// <summary>
/// Backup header
/// </summary>
public NCCHHeader BackupHeader => Model.CardInfoHeader.InitialData.BackupHeader;
/// <summary>
/// ExeFS headers
/// </summary>
public ExeFSHeader[] ExeFSHeaders => Model.ExeFSHeaders;
/// <summary>
/// Media unit size in bytes
/// </summary>
public uint MediaUnitSize
=> (uint)(0x200 * Math.Pow(2, Model.Header.PartitionFlags[(int)NCSDFlags.MediaUnitSize]));
/// <summary>
/// Partitions data table
/// </summary>
public NCCHHeader[] Partitions => Model.Partitions;
/// <summary>
/// Partitions header table
/// </summary>
public PartitionTableEntry[] PartitionsTable => Model.Header.PartitionsTable;
#region Named Partition Entries
/// <summary>
/// Partition table entry for Executable Content (CXI)
/// </summary>
public PartitionTableEntry? ExecutableContentEntry
{
get
{
if (PartitionsTable.Length < 1)
return null;
return PartitionsTable[0];
}
}
/// <summary>
/// Partition table entry for E-Manual (CFA)
/// </summary>
public PartitionTableEntry? EManualEntry
{
get
{
if (PartitionsTable.Length < 2)
return null;
return PartitionsTable[1];
}
}
/// <summary>
/// Partition table entry for Download Play Child container (CFA)
/// </summary>
public PartitionTableEntry? DownloadPlayChildContainerEntry
{
get
{
if (PartitionsTable.Length < 3)
return null;
return PartitionsTable[2];
}
}
/// <summary>
/// Partition table entry for New3DS Update Data (CFA)
/// </summary>
public PartitionTableEntry? New3DSUpdateDataEntry
{
get
{
if (PartitionsTable.Length < 7)
return null;
return PartitionsTable[6];
}
}
/// <summary>
/// Partition table entry for Update Data (CFA)
/// </summary>
public PartitionTableEntry? UpdateDataEntry
{
get
{
if (PartitionsTable.Length < 8)
return null;
return PartitionsTable[7];
}
}
#endregion
/// <summary>
/// Partitions flags
/// </summary>
public byte[] PartitionFlags => Model.Header.PartitionFlags;
#region Partition Flags
/// <summary>
/// Backup Write Wait Time (The time to wait to write save to backup after the card is recognized (0-255
/// seconds)). NATIVE_FIRM loads this flag from the gamecard NCSD header starting with 6.0.0-11.
/// </summary>
public byte BackupWriteWaitTime
{
get
{
if (PartitionFlags.Length < (int)NCSDFlags.BackupWriteWaitTime + 1)
return default;
return PartitionFlags[(int)NCSDFlags.BackupWriteWaitTime];
}
}
/// <summary>
/// Media Card Device (1 = NOR Flash, 2 = None, 3 = BT) (Only SDK 2.X)
/// </summary>
public MediaCardDeviceType MediaCardDevice2X
{
get
{
if (PartitionFlags.Length < (int)NCSDFlags.MediaCardDevice2X + 1)
return default;
return (MediaCardDeviceType)PartitionFlags[(int)NCSDFlags.MediaCardDevice2X];
}
}
/// <summary>
/// Media Card Device (1 = NOR Flash, 2 = None, 3 = BT) (SDK 3.X+)
/// </summary>
public MediaCardDeviceType MediaCardDevice3X
{
get
{
if (PartitionFlags.Length < (int)NCSDFlags.MediaCardDevice3X + 1)
return default;
return (MediaCardDeviceType)PartitionFlags[(int)NCSDFlags.MediaCardDevice3X];
}
}
/// <summary>
/// Media Platform Index (1 = CTR)
/// </summary>
public MediaPlatformIndex MediaPlatformIndex
{
get
{
if (PartitionFlags.Length < (int)NCSDFlags.MediaPlatformIndex + 1)
return default;
return (MediaPlatformIndex)PartitionFlags[(int)NCSDFlags.MediaPlatformIndex];
}
}
/// <summary>
/// Media Type Index (0 = Inner Device, 1 = Card1, 2 = Card2, 3 = Extended Device)
/// </summary>
public MediaTypeIndex MediaTypeIndex
{
get
{
if (PartitionFlags.Length < (int)NCSDFlags.MediaTypeIndex + 1)
return default;
return (MediaTypeIndex)PartitionFlags[(int)NCSDFlags.MediaTypeIndex];
}
}
#endregion
#endregion
#region Constructors
/// <inheritdoc/>
public N3DS(Cart model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public N3DS(Cart model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public N3DS(Cart model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public N3DS(Cart model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public N3DS(Cart model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public N3DS(Cart model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a 3DS cart image from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the archive</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>A 3DS cart image wrapper on success, null on failure</returns>
public static N3DS? Create(byte[]? data, int offset)
{
// If the data is invalid
if (data is null || data.Length == 0)
return null;
// If the offset is out of bounds
if (offset < 0 || offset >= data.Length)
return null;
// Create a memory stream and use that
var dataStream = new MemoryStream(data, offset, data.Length - offset);
return Create(dataStream);
}
/// <summary>
/// Create a 3DS cart image from a Stream
/// </summary>
/// <param name="data">Stream representing the archive</param>
/// <returns>A 3DS cart image wrapper on success, null on failure</returns>
public static N3DS? Create(Stream? data)
{
// If the data is invalid
if (data is null || !data.CanRead)
return null;
try
{
// Cache the current offset
long currentOffset = data.Position;
var model = new Serialization.Readers.N3DS().Deserialize(data);
if (model is null)
return null;
return new N3DS(model, data, currentOffset);
}
catch
{
return null;
}
}
#endregion
#region Data
/// <summary>
/// Get the bit masks for a partition
/// </summary>
public BitMasks GetBitMasks(int index)
{
if (Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
var partition = Partitions[index];
if (partition?.Flags is null)
return 0;
return partition.Flags.BitMasks;
}
/// <summary>
/// Get the crypto method for a partition
/// </summary>
public CryptoMethod GetCryptoMethod(int index)
{
if (Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
var partition = Partitions[index];
if (partition?.Flags is null)
return 0;
return partition.Flags.CryptoMethod;
}
/// <summary>
/// Determines if a file header represents a CODE block
/// </summary>
public bool IsCodeBinary(int fsIndex, int headerIndex)
{
if (ExeFSHeaders is null)
return false;
if (fsIndex < 0 || fsIndex >= ExeFSHeaders.Length)
return false;
var fsHeader = ExeFSHeaders[fsIndex];
if (fsHeader?.FileHeaders is null)
return false;
if (headerIndex < 0 || headerIndex >= fsHeader.FileHeaders.Length)
return false;
var fileHeader = fsHeader.FileHeaders[headerIndex];
if (fileHeader is null)
return false;
return fileHeader.FileName == ".code";
}
/// <summary>
/// Get if the NoCrypto bit is set
/// </summary>
public bool PossiblyDecrypted(int index)
{
var bitMasks = GetBitMasks(index);
#if NET20 || NET35
return (bitMasks & BitMasks.NoCrypto) != 0;
#else
return bitMasks.HasFlag(BitMasks.NoCrypto);
#endif
}
#endregion
#region Offsets
/// <summary>
/// Get the offset of a partition ExeFS
/// </summary>
/// <returns>Offset to the ExeFS of the partition, 0 on error</returns>
public uint GetExeFSOffset(int index)
{
// No partitions means no size is available
if (PartitionsTable is null || Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
// Invalid partition means no size is available
var entry = PartitionsTable[index];
var header = Partitions[index];
if (header is null || header.MagicID != NCCHMagicNumber)
return 0;
// If the offset is 0, return 0
uint exeFsOffsetMU = header.ExeFSOffsetInMediaUnits;
if (exeFsOffsetMU == 0)
return 0;
// Return the adjusted offset
uint partitionOffsetMU = entry.Offset;
return (partitionOffsetMU + exeFsOffsetMU) * MediaUnitSize;
}
/// <summary>
/// Get the offset of a partition logo region
/// </summary>
/// <returns>Offset to the logo region of the partition, 0 on error</returns>
public uint GetLogoRegionOffset(int index)
{
// No partitions means no size is available
if (PartitionsTable is null || Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
// Invalid partition means no size is available
var entry = PartitionsTable[index];
var header = Partitions[index];
if (header is null || header.MagicID != NCCHMagicNumber)
return 0;
// If the offset is 0, return 0
uint logoOffsetMU = header.LogoRegionOffsetInMediaUnits;
if (logoOffsetMU == 0)
return 0;
// Return the adjusted offset
uint partitionOffsetMU = entry.Offset;
return (partitionOffsetMU + logoOffsetMU) * MediaUnitSize;
}
/// <summary>
/// Get the offset of a partition
/// </summary>
/// <returns>Offset to the partition, 0 on error</returns>
public uint GetPartitionOffset(int index)
{
// No partitions means no size is available
if (PartitionsTable is null)
return 0;
if (index < 0 || index >= PartitionsTable.Length)
return 0;
// Return the adjusted offset
var entry = PartitionsTable[index];
uint partitionOffsetMU = entry.Offset;
if (entry.Offset == 0)
return 0;
// Return the adjusted offset
return partitionOffsetMU * MediaUnitSize;
}
/// <summary>
/// Get the offset of a partition plain region
/// </summary>
/// <returns>Offset to the plain region of the partition, 0 on error</returns>
public uint GetPlainRegionOffset(int index)
{
// No partitions means no size is available
if (PartitionsTable is null || Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
// Invalid partition means no size is available
var entry = PartitionsTable[index];
var header = Partitions[index];
if (header is null || header.MagicID != NCCHMagicNumber)
return 0;
// If the offset is 0, return 0
uint prOffsetMU = header.PlainRegionOffsetInMediaUnits;
if (prOffsetMU == 0)
return 0;
// Return the adjusted offset
uint partitionOffsetMU = entry.Offset;
return (partitionOffsetMU + prOffsetMU) * MediaUnitSize;
}
/// <summary>
/// Get the offset of a partition RomFS
/// </summary>
/// <returns>Offset to the RomFS of the partition, 0 on error</returns>
public uint GetRomFSOffset(int index)
{
// No partitions means no size is available
if (PartitionsTable is null || Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
// Invalid partition means no size is available
var entry = PartitionsTable[index];
var header = Partitions[index];
if (header is null || header.MagicID != NCCHMagicNumber)
return 0;
// If the offset is 0, return 0
uint romFsOffsetMU = header.RomFSOffsetInMediaUnits;
if (romFsOffsetMU == 0)
return 0;
// Return the adjusted offset
uint partitionOffsetMU = entry.Offset;
return (partitionOffsetMU + romFsOffsetMU) * MediaUnitSize;
}
#endregion
#region Sizes
/// <summary>
/// Get the size of a partition ExeFS
/// </summary>
/// <returns>Size of the partition ExeFS in bytes, 0 on error</returns>
public uint GetExeFSSize(int index)
{
// Empty partitions array means no size is available
if (Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
// Invalid partition header means no size is available
var header = Partitions[index];
if (header is null)
return 0;
// Return the adjusted size
return header.ExeFSSizeInMediaUnits * MediaUnitSize;
}
/// <summary>
/// Get the size of a partition extended header
/// </summary>
/// <returns>Size of the partition extended header in bytes, 0 on error</returns>
public uint GetExtendedHeaderSize(int index)
{
// Empty partitions array means no size is available
if (Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
// Invalid partition header means no size is available
var header = Partitions[index];
if (header is null)
return 0;
// Return the adjusted size
return header.ExtendedHeaderSizeInBytes;
}
/// <summary>
/// Get the size of a partition logo region
/// </summary>
/// <returns>Size of the partition logo region in bytes, 0 on error</returns>
public uint GetLogoRegionSize(int index)
{
// Empty partitions array means no size is available
if (Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
// Invalid partition header means no size is available
var header = Partitions[index];
if (header is null)
return 0;
// Return the adjusted size
return header.LogoRegionSizeInMediaUnits * MediaUnitSize;
}
/// <summary>
/// Get the size of a partition plain region
/// </summary>
/// <returns>Size of the partition plain region in bytes, 0 on error</returns>
public uint GetPlainRegionSize(int index)
{
// Empty partitions array means no size is available
if (Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
// Invalid partition header means no size is available
var header = Partitions[index];
if (header is null)
return 0;
// Return the adjusted size
return header.PlainRegionSizeInMediaUnits * MediaUnitSize;
}
/// <summary>
/// Get the size of a partition RomFS
/// </summary>
/// <returns>Size of the partition RomFS in bytes, 0 on error</returns>
public uint GetRomFSSize(int index)
{
// Empty partitions array means no size is available
if (Partitions is null)
return 0;
if (index < 0 || index >= Partitions.Length)
return 0;
// Invalid partition header means no size is available
var header = Partitions[index];
if (header is null)
return 0;
// Return the adjusted size
return header.RomFSSizeInMediaUnits * MediaUnitSize;
}
#endregion
}
}