mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-04-05 22:01:33 +00:00
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.
599 lines
19 KiB
C#
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
|
|
}
|
|
}
|