mirror of
https://github.com/SabreTools/BinaryObjectScanner.git
synced 2026-02-11 13:45:25 +00:00
1224 lines
48 KiB
C#
1224 lines
48 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Text;
|
|
using BinaryObjectScanner.Models.N3DS;
|
|
using BinaryObjectScanner.Utilities;
|
|
using static BinaryObjectScanner.Models.N3DS.Constants;
|
|
|
|
namespace BinaryObjectScanner.Builders
|
|
{
|
|
public class N3DS
|
|
{
|
|
#region Byte Data
|
|
|
|
/// <summary>
|
|
/// Parse a byte array into a 3DS cart image
|
|
/// </summary>
|
|
/// <param name="data">Byte array to parse</param>
|
|
/// <param name="offset">Offset into the byte array</param>
|
|
/// <returns>Filled cart image on success, null on error</returns>
|
|
public static Cart ParseCart(byte[] data, int offset)
|
|
{
|
|
// 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 ParseCart(dataStream);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a byte array into a CIA archive
|
|
/// </summary>
|
|
/// <param name="data">Byte array to parse</param>
|
|
/// <param name="offset">Offset into the byte array</param>
|
|
/// <returns>Filled CIA archive on success, null on error</returns>
|
|
public static CIA ParseCIA(byte[] data, int offset)
|
|
{
|
|
// 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 ParseCIA(dataStream);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Stream Data
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a 3DS cart image
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled cart image on success, null on error</returns>
|
|
public static Cart ParseCart(Stream data)
|
|
{
|
|
// 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 NCSD Header
|
|
|
|
// Try to parse the header
|
|
var header = ParseNCSDHeader(data);
|
|
if (header == null)
|
|
return null;
|
|
|
|
// Set the cart image header
|
|
cart.Header = header;
|
|
|
|
#endregion
|
|
|
|
#region Card Info Header
|
|
|
|
// Try to parse the card info header
|
|
var cardInfoHeader = ParseCardInfoHeader(data);
|
|
if (cardInfoHeader == null)
|
|
return null;
|
|
|
|
// Set the card info header
|
|
cart.CardInfoHeader = cardInfoHeader;
|
|
|
|
#endregion
|
|
|
|
#region Development Card Info Header
|
|
|
|
// Try to parse the development card info header
|
|
var developmentCardInfoHeader = ParseDevelopmentCardInfoHeader(data);
|
|
if (developmentCardInfoHeader == null)
|
|
return null;
|
|
|
|
// Set the development card info header
|
|
cart.DevelopmentCardInfoHeader = developmentCardInfoHeader;
|
|
|
|
#endregion
|
|
|
|
#region Partitions
|
|
|
|
// Create the partition table
|
|
cart.Partitions = new NCCHHeader[8];
|
|
|
|
// Iterate and build the partitions
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
cart.Partitions[i] = ParseNCCHHeader(data);
|
|
}
|
|
|
|
#endregion
|
|
|
|
// Cache the media unit size for further use
|
|
long mediaUnitSize = (uint)(0x200 * Math.Pow(2, header.PartitionFlags[(int)NCSDFlags.MediaUnitSize]));
|
|
|
|
#region Extended Headers
|
|
|
|
// Create the extended header table
|
|
cart.ExtendedHeaders = new NCCHExtendedHeader[8];
|
|
|
|
// Iterate and build the extended headers
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
// If we have an encrypted or invalid partition
|
|
if (cart.Partitions[i].MagicID != NCCHMagicNumber)
|
|
continue;
|
|
|
|
// Get the extended header offset
|
|
long offset = (cart.Header.PartitionsTable[i].Offset * mediaUnitSize) + 0x200;
|
|
if (offset < 0 || offset >= data.Length)
|
|
continue;
|
|
|
|
// Seek to the extended header
|
|
data.Seek(offset, SeekOrigin.Begin);
|
|
|
|
// Parse the extended header
|
|
cart.ExtendedHeaders[i] = ParseNCCHExtendedHeader(data);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ExeFS Headers
|
|
|
|
// Create the ExeFS header table
|
|
cart.ExeFSHeaders = new ExeFSHeader[8];
|
|
|
|
// Iterate and build the ExeFS headers
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
// If we have an encrypted or invalid partition
|
|
if (cart.Partitions[i].MagicID != NCCHMagicNumber)
|
|
continue;
|
|
|
|
// Get the ExeFS header offset
|
|
long offset = (cart.Header.PartitionsTable[i].Offset + cart.Partitions[i].ExeFSOffsetInMediaUnits) * mediaUnitSize;
|
|
if (offset < 0 || offset >= data.Length)
|
|
continue;
|
|
|
|
// Seek to the ExeFS header
|
|
data.Seek(offset, SeekOrigin.Begin);
|
|
|
|
// Parse the ExeFS header
|
|
cart.ExeFSHeaders[i] = ParseExeFSHeader(data);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region RomFS Headers
|
|
|
|
// Create the RomFS header table
|
|
cart.RomFSHeaders = new RomFSHeader[8];
|
|
|
|
// Iterate and build the RomFS headers
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
// If we have an encrypted or invalid partition
|
|
if (cart.Partitions[i].MagicID != NCCHMagicNumber)
|
|
continue;
|
|
|
|
// Get the RomFS header offset
|
|
long offset = (cart.Header.PartitionsTable[i].Offset + cart.Partitions[i].RomFSOffsetInMediaUnits) * mediaUnitSize;
|
|
if (offset < 0 || offset >= data.Length)
|
|
continue;
|
|
|
|
// Seek to the RomFS header
|
|
data.Seek(offset, SeekOrigin.Begin);
|
|
|
|
// Parse the RomFS header
|
|
cart.RomFSHeaders[i] = ParseRomFSHeader(data);
|
|
}
|
|
|
|
#endregion
|
|
|
|
return cart;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a CIA archive
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled CIA archive on success, null on error</returns>
|
|
public static CIA ParseCIA(Stream data)
|
|
{
|
|
// 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 CIA archive to fill
|
|
var cia = new CIA();
|
|
|
|
#region CIA Header
|
|
|
|
// Try to parse the header
|
|
var header = ParseCIAHeader(data);
|
|
if (header == null)
|
|
return null;
|
|
|
|
// Set the CIA archive header
|
|
cia.Header = header;
|
|
|
|
#endregion
|
|
|
|
// Align to 64-byte boundary, if needed
|
|
while (data.Position < data.Length - 1 && data.Position % 64 != 0)
|
|
{
|
|
_ = data.ReadByteValue();
|
|
}
|
|
|
|
#region Certificate Chain
|
|
|
|
// Create the certificate chain
|
|
cia.CertificateChain = new Certificate[3];
|
|
|
|
// Try to parse the certificates
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
var certificate = ParseCertificate(data);
|
|
if (certificate == null)
|
|
return null;
|
|
|
|
cia.CertificateChain[i] = certificate;
|
|
}
|
|
|
|
#endregion
|
|
|
|
// Align to 64-byte boundary, if needed
|
|
while (data.Position < data.Length - 1 && data.Position % 64 != 0)
|
|
{
|
|
_ = data.ReadByteValue();
|
|
}
|
|
|
|
#region Ticket
|
|
|
|
// Try to parse the ticket
|
|
var ticket = ParseTicket(data);
|
|
if (ticket == null)
|
|
return null;
|
|
|
|
// Set the ticket
|
|
cia.Ticket = ticket;
|
|
|
|
#endregion
|
|
|
|
// Align to 64-byte boundary, if needed
|
|
while (data.Position < data.Length - 1 && data.Position % 64 != 0)
|
|
{
|
|
_ = data.ReadByteValue();
|
|
}
|
|
|
|
#region Title Metadata
|
|
|
|
// Try to parse the title metadata
|
|
var titleMetadata = ParseTitleMetadata(data);
|
|
if (titleMetadata == null)
|
|
return null;
|
|
|
|
// Set the title metadata
|
|
cia.TMDFileData = titleMetadata;
|
|
|
|
#endregion
|
|
|
|
// Align to 64-byte boundary, if needed
|
|
while (data.Position < data.Length - 1 && data.Position % 64 != 0)
|
|
{
|
|
_ = data.ReadByteValue();
|
|
}
|
|
|
|
#region Content File Data
|
|
|
|
// Create the partition table
|
|
cia.Partitions = new NCCHHeader[8];
|
|
|
|
// Iterate and build the partitions
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
cia.Partitions[i] = ParseNCCHHeader(data);
|
|
}
|
|
|
|
#endregion
|
|
|
|
// Align to 64-byte boundary, if needed
|
|
while (data.Position < data.Length - 1 && data.Position % 64 != 0)
|
|
{
|
|
_ = data.ReadByteValue();
|
|
}
|
|
|
|
#region Meta Data
|
|
|
|
// If we have a meta data
|
|
if (header.MetaSize > 0)
|
|
{
|
|
// Try to parse the meta
|
|
var meta = ParseMetaData(data);
|
|
if (meta == null)
|
|
return null;
|
|
|
|
// Set the meta
|
|
cia.MetaData = meta;
|
|
}
|
|
|
|
#endregion
|
|
|
|
return cia;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an NCSD header
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled NCSD header on success, null on error</returns>
|
|
private static NCSDHeader ParseNCSDHeader(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
NCSDHeader header = new NCSDHeader();
|
|
|
|
header.RSA2048Signature = data.ReadBytes(0x100);
|
|
byte[] magicNumber = data.ReadBytes(4);
|
|
header.MagicNumber = Encoding.ASCII.GetString(magicNumber).TrimEnd('\0'); ;
|
|
if (header.MagicNumber != NCSDMagicNumber)
|
|
return null;
|
|
|
|
header.ImageSizeInMediaUnits = data.ReadUInt32();
|
|
header.MediaId = data.ReadBytes(8);
|
|
header.PartitionsFSType = (FilesystemType)data.ReadUInt64();
|
|
header.PartitionsCryptType = data.ReadBytes(8);
|
|
|
|
header.PartitionsTable = new PartitionTableEntry[8];
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
header.PartitionsTable[i] = ParsePartitionTableEntry(data);
|
|
}
|
|
|
|
if (header.PartitionsFSType == FilesystemType.Normal || header.PartitionsFSType == FilesystemType.None)
|
|
{
|
|
header.ExheaderHash = data.ReadBytes(0x20);
|
|
header.AdditionalHeaderSize = data.ReadUInt32();
|
|
header.SectorZeroOffset = data.ReadUInt32();
|
|
header.PartitionFlags = data.ReadBytes(8);
|
|
|
|
header.PartitionIdTable = new ulong[8];
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
header.PartitionIdTable[i] = data.ReadUInt64();
|
|
}
|
|
|
|
header.Reserved1 = data.ReadBytes(0x20);
|
|
header.Reserved2 = data.ReadBytes(0x0E);
|
|
header.FirmUpdateByte1 = data.ReadByteValue();
|
|
header.FirmUpdateByte2 = data.ReadByteValue();
|
|
}
|
|
else if (header.PartitionsFSType == FilesystemType.FIRM)
|
|
{
|
|
header.Unknown = data.ReadBytes(0x5E);
|
|
header.EncryptedMBR = data.ReadBytes(0x42);
|
|
}
|
|
|
|
return header;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a partition table entry
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled partition table entry on success, null on error</returns>
|
|
private static PartitionTableEntry ParsePartitionTableEntry(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
PartitionTableEntry partitionTableEntry = new PartitionTableEntry();
|
|
|
|
partitionTableEntry.Offset = data.ReadUInt32();
|
|
partitionTableEntry.Length = data.ReadUInt32();
|
|
|
|
return partitionTableEntry;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a card info header
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled card info header on success, null on error</returns>
|
|
private static CardInfoHeader ParseCardInfoHeader(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
CardInfoHeader cardInfoHeader = new CardInfoHeader();
|
|
|
|
cardInfoHeader.WritableAddressMediaUnits = data.ReadUInt32();
|
|
cardInfoHeader.CardInfoBitmask = data.ReadUInt32();
|
|
cardInfoHeader.Reserved1 = data.ReadBytes(0xF8);
|
|
cardInfoHeader.FilledSize = data.ReadUInt32();
|
|
cardInfoHeader.Reserved2 = data.ReadBytes(0x0C);
|
|
cardInfoHeader.TitleVersion = data.ReadUInt16();
|
|
cardInfoHeader.CardRevision = data.ReadUInt16();
|
|
cardInfoHeader.Reserved3 = data.ReadBytes(0x0C);
|
|
cardInfoHeader.CVerTitleID = data.ReadBytes(8);
|
|
cardInfoHeader.CVerVersionNumber = data.ReadUInt16();
|
|
cardInfoHeader.Reserved4 = data.ReadBytes(0xCD6);
|
|
|
|
return cardInfoHeader;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a development card info header
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled development card info header on success, null on error</returns>
|
|
private static DevelopmentCardInfoHeader ParseDevelopmentCardInfoHeader(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
DevelopmentCardInfoHeader developmentCardInfoHeader = new DevelopmentCardInfoHeader();
|
|
|
|
developmentCardInfoHeader.InitialData = ParseInitialData(data);
|
|
if (developmentCardInfoHeader.InitialData == null)
|
|
return null;
|
|
|
|
developmentCardInfoHeader.CardDeviceReserved1 = data.ReadBytes(0x200);
|
|
developmentCardInfoHeader.TitleKey = data.ReadBytes(0x10);
|
|
developmentCardInfoHeader.CardDeviceReserved2 = data.ReadBytes(0x1BF0);
|
|
|
|
developmentCardInfoHeader.TestData = ParseTestData(data);
|
|
if (developmentCardInfoHeader.TestData == null)
|
|
return null;
|
|
|
|
return developmentCardInfoHeader;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an initial data
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled initial data on success, null on error</returns>
|
|
private static InitialData ParseInitialData(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
InitialData initialData = new InitialData();
|
|
|
|
initialData.CardSeedKeyY = data.ReadBytes(0x10);
|
|
initialData.EncryptedCardSeed = data.ReadBytes(0x10);
|
|
initialData.CardSeedAESMAC = data.ReadBytes(0x10);
|
|
initialData.CardSeedNonce = data.ReadBytes(0xC);
|
|
initialData.Reserved = data.ReadBytes(0xC4);
|
|
initialData.BackupHeader = ParseNCCHHeader(data, true);
|
|
if (initialData.BackupHeader == null)
|
|
return null;
|
|
|
|
return initialData;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an NCCH header
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <param name="skipSignature">Indicates if the signature should be skipped</param>
|
|
/// <returns>Filled NCCH header on success, null on error</returns>
|
|
private static NCCHHeader ParseNCCHHeader(Stream data, bool skipSignature = false)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
NCCHHeader header = new NCCHHeader();
|
|
|
|
if (!skipSignature)
|
|
header.RSA2048Signature = data.ReadBytes(0x100);
|
|
|
|
byte[] magicId = data.ReadBytes(4);
|
|
header.MagicID = Encoding.ASCII.GetString(magicId).TrimEnd('\0');
|
|
header.ContentSizeInMediaUnits = data.ReadUInt32();
|
|
header.PartitionId = data.ReadUInt64();
|
|
header.MakerCode = data.ReadUInt16();
|
|
header.Version = data.ReadUInt16();
|
|
header.VerificationHash = data.ReadUInt32();
|
|
header.ProgramId = data.ReadBytes(8);
|
|
header.Reserved1 = data.ReadBytes(0x10);
|
|
header.LogoRegionHash = data.ReadBytes(0x20);
|
|
byte[] productCode = data.ReadBytes(0x10);
|
|
header.ProductCode = Encoding.ASCII.GetString(productCode).TrimEnd('\0');
|
|
header.ExtendedHeaderHash = data.ReadBytes(0x20);
|
|
header.ExtendedHeaderSizeInBytes = data.ReadUInt32();
|
|
header.Reserved2 = data.ReadBytes(4);
|
|
header.Flags = ParseNCCHHeaderFlags(data);
|
|
header.PlainRegionOffsetInMediaUnits = data.ReadUInt32();
|
|
header.PlainRegionSizeInMediaUnits = data.ReadUInt32();
|
|
header.LogoRegionOffsetInMediaUnits = data.ReadUInt32();
|
|
header.LogoRegionSizeInMediaUnits = data.ReadUInt32();
|
|
header.ExeFSOffsetInMediaUnits = data.ReadUInt32();
|
|
header.ExeFSSizeInMediaUnits = data.ReadUInt32();
|
|
header.ExeFSHashRegionSizeInMediaUnits = data.ReadUInt32();
|
|
header.Reserved3 = data.ReadBytes(4);
|
|
header.RomFSOffsetInMediaUnits = data.ReadUInt32();
|
|
header.RomFSSizeInMediaUnits = data.ReadUInt32();
|
|
header.RomFSHashRegionSizeInMediaUnits = data.ReadUInt32();
|
|
header.Reserved4 = data.ReadBytes(4);
|
|
header.ExeFSSuperblockHash = data.ReadBytes(0x20);
|
|
header.RomFSSuperblockHash = data.ReadBytes(0x20);
|
|
|
|
return header;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an NCCH header flags
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled NCCH header flags on success, null on error</returns>
|
|
private static NCCHHeaderFlags ParseNCCHHeaderFlags(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
NCCHHeaderFlags headerFlags = new NCCHHeaderFlags();
|
|
|
|
headerFlags.Reserved0 = data.ReadByteValue();
|
|
headerFlags.Reserved1 = data.ReadByteValue();
|
|
headerFlags.Reserved2 = data.ReadByteValue();
|
|
headerFlags.CryptoMethod = (CryptoMethod)data.ReadByteValue();
|
|
headerFlags.ContentPlatform = (ContentPlatform)data.ReadByteValue();
|
|
headerFlags.MediaPlatformIndex = (ContentType)data.ReadByteValue();
|
|
headerFlags.ContentUnitSize = data.ReadByteValue();
|
|
headerFlags.BitMasks = (BitMasks)data.ReadByteValue();
|
|
|
|
return headerFlags;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an initial data
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled initial data on success, null on error</returns>
|
|
private static TestData ParseTestData(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
TestData testData = new TestData();
|
|
|
|
// TODO: Validate some of the values
|
|
testData.Signature = data.ReadBytes(8);
|
|
testData.AscendingByteSequence = data.ReadBytes(0x1F8);
|
|
testData.DescendingByteSequence = data.ReadBytes(0x200);
|
|
testData.Filled00 = data.ReadBytes(0x200);
|
|
testData.FilledFF = data.ReadBytes(0x200);
|
|
testData.Filled0F = data.ReadBytes(0x200);
|
|
testData.FilledF0 = data.ReadBytes(0x200);
|
|
testData.Filled55 = data.ReadBytes(0x200);
|
|
testData.FilledAA = data.ReadBytes(0x1FF);
|
|
testData.FinalByte = data.ReadByteValue();
|
|
|
|
return testData;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an NCCH extended header
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled NCCH extended header on success, null on error</returns>
|
|
private static NCCHExtendedHeader ParseNCCHExtendedHeader(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
NCCHExtendedHeader extendedHeader = new NCCHExtendedHeader();
|
|
|
|
extendedHeader.SCI = ParseSystemControlInfo(data);
|
|
if (extendedHeader.SCI == null)
|
|
return null;
|
|
|
|
extendedHeader.ACI = ParseAccessControlInfo(data);
|
|
if (extendedHeader.ACI == null)
|
|
return null;
|
|
|
|
extendedHeader.AccessDescSignature = data.ReadBytes(0x100);
|
|
extendedHeader.NCCHHDRPublicKey = data.ReadBytes(0x100);
|
|
|
|
extendedHeader.ACIForLimitations = ParseAccessControlInfo(data);
|
|
if (extendedHeader.ACI == null)
|
|
return null;
|
|
|
|
return extendedHeader;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a system control info
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled system control info on success, null on error</returns>
|
|
private static SystemControlInfo ParseSystemControlInfo(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
SystemControlInfo systemControlInfo = new SystemControlInfo();
|
|
|
|
byte[] applicationTitle = data.ReadBytes(8);
|
|
systemControlInfo.ApplicationTitle = Encoding.ASCII.GetString(applicationTitle).TrimEnd('\0');
|
|
systemControlInfo.Reserved1 = data.ReadBytes(5);
|
|
systemControlInfo.Flag = data.ReadByteValue();
|
|
systemControlInfo.RemasterVersion = data.ReadUInt16();
|
|
systemControlInfo.TextCodeSetInfo = ParseCodeSetInfo(data);
|
|
systemControlInfo.StackSize = data.ReadUInt32();
|
|
systemControlInfo.ReadOnlyCodeSetInfo = ParseCodeSetInfo(data);
|
|
systemControlInfo.Reserved2 = data.ReadBytes(4);
|
|
systemControlInfo.DataCodeSetInfo = ParseCodeSetInfo(data);
|
|
systemControlInfo.BSSSize = data.ReadUInt32();
|
|
systemControlInfo.DependencyModuleList = new ulong[48];
|
|
for (int i = 0; i < 48; i++)
|
|
{
|
|
systemControlInfo.DependencyModuleList[i] = data.ReadUInt64();
|
|
}
|
|
systemControlInfo.SystemInfo = ParseSystemInfo(data);
|
|
|
|
return systemControlInfo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a code set info
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled code set info on success, null on error</returns>
|
|
private static CodeSetInfo ParseCodeSetInfo(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
CodeSetInfo codeSetInfo = new CodeSetInfo();
|
|
|
|
codeSetInfo.Address = data.ReadUInt32();
|
|
codeSetInfo.PhysicalRegionSizeInPages = data.ReadUInt32();
|
|
codeSetInfo.SizeInBytes = data.ReadUInt32();
|
|
|
|
return codeSetInfo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a system info
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled system info on success, null on error</returns>
|
|
private static SystemInfo ParseSystemInfo(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
SystemInfo systemInfo = new SystemInfo();
|
|
|
|
systemInfo.SaveDataSize = data.ReadUInt64();
|
|
systemInfo.JumpID = data.ReadUInt64();
|
|
systemInfo.Reserved = data.ReadBytes(0x30);
|
|
|
|
return systemInfo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an access control info
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled access control info on success, null on error</returns>
|
|
private static AccessControlInfo ParseAccessControlInfo(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
AccessControlInfo accessControlInfo = new AccessControlInfo();
|
|
|
|
accessControlInfo.ARM11LocalSystemCapabilities = ParseARM11LocalSystemCapabilities(data);
|
|
accessControlInfo.ARM11KernelCapabilities = ParseARM11KernelCapabilities(data);
|
|
accessControlInfo.ARM9AccessControl = ParseARM9AccessControl(data);
|
|
|
|
return accessControlInfo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an ARM11 local system capabilities
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled ARM11 local system capabilities on success, null on error</returns>
|
|
private static ARM11LocalSystemCapabilities ParseARM11LocalSystemCapabilities(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
ARM11LocalSystemCapabilities arm11LocalSystemCapabilities = new ARM11LocalSystemCapabilities();
|
|
|
|
arm11LocalSystemCapabilities.ProgramID = data.ReadUInt64();
|
|
arm11LocalSystemCapabilities.CoreVersion = data.ReadUInt32();
|
|
arm11LocalSystemCapabilities.Flag1 = (ARM11LSCFlag1)data.ReadByteValue();
|
|
arm11LocalSystemCapabilities.Flag2 = (ARM11LSCFlag2)data.ReadByteValue();
|
|
arm11LocalSystemCapabilities.Flag0 = (ARM11LSCFlag0)data.ReadByteValue();
|
|
arm11LocalSystemCapabilities.Priority = data.ReadByteValue();
|
|
arm11LocalSystemCapabilities.ResourceLimitDescriptors = new ushort[16];
|
|
for (int i = 0; i < 16; i++)
|
|
{
|
|
arm11LocalSystemCapabilities.ResourceLimitDescriptors[i] = data.ReadUInt16();
|
|
}
|
|
arm11LocalSystemCapabilities.StorageInfo = ParseStorageInfo(data);
|
|
arm11LocalSystemCapabilities.ServiceAccessControl = new ulong[32];
|
|
for (int i = 0; i < 32; i++)
|
|
{
|
|
arm11LocalSystemCapabilities.ServiceAccessControl[i] = data.ReadUInt64();
|
|
}
|
|
arm11LocalSystemCapabilities.ExtendedServiceAccessControl = new ulong[2];
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
arm11LocalSystemCapabilities.ExtendedServiceAccessControl[i] = data.ReadUInt64();
|
|
}
|
|
arm11LocalSystemCapabilities.Reserved = data.ReadBytes(0x0F);
|
|
arm11LocalSystemCapabilities.ResourceLimitCategory = (ResourceLimitCategory)data.ReadByteValue();
|
|
|
|
return arm11LocalSystemCapabilities;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a storage info
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled storage info on success, null on error</returns>
|
|
private static StorageInfo ParseStorageInfo(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
StorageInfo storageInfo = new StorageInfo();
|
|
|
|
storageInfo.ExtdataID = data.ReadUInt64();
|
|
storageInfo.SystemSavedataIDs = data.ReadBytes(8);
|
|
storageInfo.StorageAccessibleUniqueIDs = data.ReadBytes(8);
|
|
storageInfo.FileSystemAccessInfo = data.ReadBytes(7);
|
|
storageInfo.OtherAttributes = (StorageInfoOtherAttributes)data.ReadByteValue();
|
|
|
|
return storageInfo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an ARM11 kernel capabilities
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled ARM11 kernel capabilities on success, null on error</returns>
|
|
private static ARM11KernelCapabilities ParseARM11KernelCapabilities(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
ARM11KernelCapabilities arm11KernelCapabilities = new ARM11KernelCapabilities();
|
|
|
|
arm11KernelCapabilities.Descriptors = new uint[28];
|
|
for (int i = 0; i < 28; i++)
|
|
{
|
|
arm11KernelCapabilities.Descriptors[i] = data.ReadUInt32();
|
|
}
|
|
arm11KernelCapabilities.Reserved = data.ReadBytes(0x10);
|
|
|
|
return arm11KernelCapabilities;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an ARM11 access control
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled ARM11 access control on success, null on error</returns>
|
|
private static ARM9AccessControl ParseARM9AccessControl(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
ARM9AccessControl arm9AccessControl = new ARM9AccessControl();
|
|
|
|
arm9AccessControl.Descriptors = data.ReadBytes(15);
|
|
arm9AccessControl.DescriptorVersion = data.ReadByteValue();
|
|
|
|
return arm9AccessControl;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an ExeFS header
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled ExeFS header on success, null on error</returns>
|
|
private static ExeFSHeader ParseExeFSHeader(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
ExeFSHeader exeFSHeader = new ExeFSHeader();
|
|
|
|
exeFSHeader.FileHeaders = new ExeFSFileHeader[10];
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
exeFSHeader.FileHeaders[i] = ParseExeFSFileHeader(data);
|
|
}
|
|
exeFSHeader.Reserved = data.ReadBytes(0x20);
|
|
exeFSHeader.FileHashes = new byte[10][];
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
exeFSHeader.FileHashes[i] = data.ReadBytes(0x20);
|
|
}
|
|
|
|
return exeFSHeader;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an ExeFS file header
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled ExeFS file header on success, null on error</returns>
|
|
private static ExeFSFileHeader ParseExeFSFileHeader(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
ExeFSFileHeader exeFSFileHeader = new ExeFSFileHeader();
|
|
|
|
byte[] fileName = data.ReadBytes(8);
|
|
exeFSFileHeader.FileName = Encoding.ASCII.GetString(fileName).TrimEnd('\0');
|
|
exeFSFileHeader.FileOffset = data.ReadUInt32();
|
|
exeFSFileHeader.FileSize = data.ReadUInt32();
|
|
|
|
return exeFSFileHeader;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into an RomFS header
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled RomFS header on success, null on error</returns>
|
|
private static RomFSHeader ParseRomFSHeader(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
RomFSHeader romFSHeader = new RomFSHeader();
|
|
|
|
byte[] magicString = data.ReadBytes(4);
|
|
romFSHeader.MagicString = Encoding.ASCII.GetString(magicString).TrimEnd('\0');
|
|
if (romFSHeader.MagicString != RomFSMagicNumber)
|
|
return null;
|
|
|
|
romFSHeader.MagicNumber = data.ReadUInt32();
|
|
if (romFSHeader.MagicNumber != RomFSSecondMagicNumber)
|
|
return null;
|
|
|
|
romFSHeader.MasterHashSize = data.ReadUInt32();
|
|
romFSHeader.Level1LogicalOffset = data.ReadUInt64();
|
|
romFSHeader.Level1HashdataSize = data.ReadUInt64();
|
|
romFSHeader.Level1BlockSizeLog2 = data.ReadUInt32();
|
|
romFSHeader.Reserved1 = data.ReadBytes(4);
|
|
romFSHeader.Level2LogicalOffset = data.ReadUInt64();
|
|
romFSHeader.Level2HashdataSize = data.ReadUInt64();
|
|
romFSHeader.Level2BlockSizeLog2 = data.ReadUInt32();
|
|
romFSHeader.Reserved2 = data.ReadBytes(4);
|
|
romFSHeader.Level3LogicalOffset = data.ReadUInt64();
|
|
romFSHeader.Level3HashdataSize = data.ReadUInt64();
|
|
romFSHeader.Level3BlockSizeLog2 = data.ReadUInt32();
|
|
romFSHeader.Reserved3 = data.ReadBytes(4);
|
|
romFSHeader.Reserved4 = data.ReadBytes(4);
|
|
romFSHeader.OptionalInfoSize = data.ReadUInt32();
|
|
|
|
return romFSHeader;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a CIA header
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled CIA header on success, null on error</returns>
|
|
private static CIAHeader ParseCIAHeader(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
CIAHeader ciaHeader = new CIAHeader();
|
|
|
|
ciaHeader.HeaderSize = data.ReadUInt32();
|
|
ciaHeader.Type = data.ReadUInt16();
|
|
ciaHeader.Version = data.ReadUInt16();
|
|
ciaHeader.CertificateChainSize = data.ReadUInt32();
|
|
ciaHeader.TicketSize = data.ReadUInt32();
|
|
ciaHeader.TMDFileSize = data.ReadUInt32();
|
|
ciaHeader.MetaSize = data.ReadUInt32();
|
|
ciaHeader.ContentSize = data.ReadUInt64();
|
|
ciaHeader.ContentIndex = data.ReadBytes(0x2000);
|
|
|
|
return ciaHeader;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a certificate
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled certificate on success, null on error</returns>
|
|
private static Certificate ParseCertificate(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
Certificate certificate = new Certificate();
|
|
|
|
certificate.SignatureType = (SignatureType)data.ReadUInt32();
|
|
switch (certificate.SignatureType)
|
|
{
|
|
case SignatureType.RSA_4096_SHA1:
|
|
certificate.SignatureSize = 0x200;
|
|
certificate.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.RSA_2048_SHA1:
|
|
certificate.SignatureSize = 0x100;
|
|
certificate.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.ECDSA_SHA1:
|
|
certificate.SignatureSize = 0x3C;
|
|
certificate.PaddingSize = 0x40;
|
|
break;
|
|
case SignatureType.RSA_4096_SHA256:
|
|
certificate.SignatureSize = 0x200;
|
|
certificate.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.RSA_2048_SHA256:
|
|
certificate.SignatureSize = 0x100;
|
|
certificate.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.ECDSA_SHA256:
|
|
certificate.SignatureSize = 0x3C;
|
|
certificate.PaddingSize = 0x40;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
certificate.Signature = data.ReadBytes(certificate.SignatureSize);
|
|
certificate.Padding = data.ReadBytes(certificate.PaddingSize);
|
|
byte[] issuer = data.ReadBytes(0x40);
|
|
certificate.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0');
|
|
certificate.KeyType = (PublicKeyType)data.ReadUInt32();
|
|
byte[] name = data.ReadBytes(0x40);
|
|
certificate.Name = Encoding.ASCII.GetString(name).TrimEnd('\0');
|
|
certificate.ExpirationTime = data.ReadUInt32();
|
|
|
|
switch (certificate.KeyType)
|
|
{
|
|
case PublicKeyType.RSA_4096:
|
|
certificate.RSAModulus = data.ReadBytes(0x200);
|
|
certificate.RSAPublicExponent = data.ReadUInt32();
|
|
certificate.RSAPadding = data.ReadBytes(0x34);
|
|
break;
|
|
case PublicKeyType.RSA_2048:
|
|
certificate.RSAModulus = data.ReadBytes(0x100);
|
|
certificate.RSAPublicExponent = data.ReadUInt32();
|
|
certificate.RSAPadding = data.ReadBytes(0x34);
|
|
break;
|
|
case PublicKeyType.EllipticCurve:
|
|
certificate.ECCPublicKey = data.ReadBytes(0x3C);
|
|
certificate.ECCPadding = data.ReadBytes(0x3C);
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
return certificate;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a ticket
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <param name="fromCdn">Indicates if the ticket is from CDN</param>
|
|
/// <returns>Filled ticket on success, null on error</returns>
|
|
private static Ticket ParseTicket(Stream data, bool fromCdn = false)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
Ticket ticket = new Ticket();
|
|
|
|
ticket.SignatureType = (SignatureType)data.ReadUInt32();
|
|
switch (ticket.SignatureType)
|
|
{
|
|
case SignatureType.RSA_4096_SHA1:
|
|
ticket.SignatureSize = 0x200;
|
|
ticket.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.RSA_2048_SHA1:
|
|
ticket.SignatureSize = 0x100;
|
|
ticket.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.ECDSA_SHA1:
|
|
ticket.SignatureSize = 0x3C;
|
|
ticket.PaddingSize = 0x40;
|
|
break;
|
|
case SignatureType.RSA_4096_SHA256:
|
|
ticket.SignatureSize = 0x200;
|
|
ticket.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.RSA_2048_SHA256:
|
|
ticket.SignatureSize = 0x100;
|
|
ticket.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.ECDSA_SHA256:
|
|
ticket.SignatureSize = 0x3C;
|
|
ticket.PaddingSize = 0x40;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
ticket.Signature = data.ReadBytes(ticket.SignatureSize);
|
|
ticket.Padding = data.ReadBytes(ticket.PaddingSize);
|
|
byte[] issuer = data.ReadBytes(0x40);
|
|
ticket.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0');
|
|
ticket.ECCPublicKey = data.ReadBytes(0x3C);
|
|
ticket.Version = data.ReadByteValue();
|
|
ticket.CaCrlVersion = data.ReadByteValue();
|
|
ticket.SignerCrlVersion = data.ReadByteValue();
|
|
ticket.TitleKey = data.ReadBytes(0x10);
|
|
ticket.Reserved1 = data.ReadByteValue();
|
|
ticket.TicketID = data.ReadUInt64();
|
|
ticket.ConsoleID = data.ReadUInt32();
|
|
ticket.TitleID = data.ReadUInt64();
|
|
ticket.Reserved2 = data.ReadBytes(2);
|
|
ticket.TicketTitleVersion = data.ReadUInt16();
|
|
ticket.Reserved3 = data.ReadBytes(8);
|
|
ticket.LicenseType = data.ReadByteValue();
|
|
ticket.CommonKeyYIndex = data.ReadByteValue();
|
|
ticket.Reserved4 = data.ReadBytes(0x2A);
|
|
ticket.eShopAccountID = data.ReadUInt32();
|
|
ticket.Reserved5 = data.ReadByteValue();
|
|
ticket.Audit = data.ReadByteValue();
|
|
ticket.Reserved6 = data.ReadBytes(0x42);
|
|
ticket.Limits = new uint[0x10];
|
|
for (int i = 0; i < ticket.Limits.Length; i++)
|
|
{
|
|
ticket.Limits[i] = data.ReadUInt32();
|
|
}
|
|
|
|
// Seek to the content index size
|
|
data.Seek(4, SeekOrigin.Current);
|
|
|
|
// Read the size (big-endian)
|
|
byte[] contentIndexSize = data.ReadBytes(4);
|
|
Array.Reverse(contentIndexSize);
|
|
ticket.ContentIndexSize = BitConverter.ToUInt32(contentIndexSize, 0);
|
|
|
|
// Seek back to the start of the content index
|
|
data.Seek(-8, SeekOrigin.Current);
|
|
|
|
ticket.ContentIndex = data.ReadBytes((int)ticket.ContentIndexSize);
|
|
|
|
// Certificates only exist in standalone CETK files
|
|
if (fromCdn)
|
|
{
|
|
ticket.CertificateChain = new Certificate[2];
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
var certificate = ParseCertificate(data);
|
|
if (certificate == null)
|
|
return null;
|
|
|
|
ticket.CertificateChain[i] = certificate;
|
|
}
|
|
}
|
|
|
|
return ticket;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a title metadata
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <param name="fromCdn">Indicates if the ticket is from CDN</param>
|
|
/// <returns>Filled title metadata on success, null on error</returns>
|
|
private static TitleMetadata ParseTitleMetadata(Stream data, bool fromCdn = false)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
TitleMetadata titleMetadata = new TitleMetadata();
|
|
|
|
titleMetadata.SignatureType = (SignatureType)data.ReadUInt32();
|
|
switch (titleMetadata.SignatureType)
|
|
{
|
|
case SignatureType.RSA_4096_SHA1:
|
|
titleMetadata.SignatureSize = 0x200;
|
|
titleMetadata.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.RSA_2048_SHA1:
|
|
titleMetadata.SignatureSize = 0x100;
|
|
titleMetadata.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.ECDSA_SHA1:
|
|
titleMetadata.SignatureSize = 0x3C;
|
|
titleMetadata.PaddingSize = 0x40;
|
|
break;
|
|
case SignatureType.RSA_4096_SHA256:
|
|
titleMetadata.SignatureSize = 0x200;
|
|
titleMetadata.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.RSA_2048_SHA256:
|
|
titleMetadata.SignatureSize = 0x100;
|
|
titleMetadata.PaddingSize = 0x3C;
|
|
break;
|
|
case SignatureType.ECDSA_SHA256:
|
|
titleMetadata.SignatureSize = 0x3C;
|
|
titleMetadata.PaddingSize = 0x40;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
titleMetadata.Signature = data.ReadBytes(titleMetadata.SignatureSize);
|
|
titleMetadata.Padding1 = data.ReadBytes(titleMetadata.PaddingSize);
|
|
byte[] issuer = data.ReadBytes(0x40);
|
|
titleMetadata.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0');
|
|
titleMetadata.Version = data.ReadByteValue();
|
|
titleMetadata.CaCrlVersion = data.ReadByteValue();
|
|
titleMetadata.SignerCrlVersion = data.ReadByteValue();
|
|
titleMetadata.Reserved1 = data.ReadByteValue();
|
|
titleMetadata.SystemVersion = data.ReadUInt64();
|
|
titleMetadata.TitleID = data.ReadUInt64();
|
|
titleMetadata.TitleType = data.ReadUInt32();
|
|
titleMetadata.GroupID = data.ReadUInt16();
|
|
titleMetadata.SaveDataSize = data.ReadUInt32();
|
|
titleMetadata.SRLPrivateSaveDataSize = data.ReadUInt32();
|
|
titleMetadata.Reserved2 = data.ReadBytes(4);
|
|
titleMetadata.SRLFlag = data.ReadByteValue();
|
|
titleMetadata.Reserved3 = data.ReadBytes(0x31);
|
|
titleMetadata.AccessRights = data.ReadUInt32();
|
|
titleMetadata.TitleVersion = data.ReadUInt16();
|
|
|
|
// Read the content count (big-endian)
|
|
byte[] contentCount = data.ReadBytes(2);
|
|
Array.Reverse(contentCount);
|
|
titleMetadata.ContentCount = BitConverter.ToUInt16(contentCount, 0);
|
|
|
|
titleMetadata.BootContent = data.ReadUInt16();
|
|
titleMetadata.Padding2 = data.ReadBytes(2);
|
|
titleMetadata.SHA256HashContentInfoRecords = data.ReadBytes(0x20);
|
|
titleMetadata.ContentInfoRecords = new ContentInfoRecord[64];
|
|
for (int i = 0; i < 64; i++)
|
|
{
|
|
titleMetadata.ContentInfoRecords[i] = ParseContentInfoRecord(data);
|
|
}
|
|
titleMetadata.ContentChunkRecords = new ContentChunkRecord[titleMetadata.ContentCount];
|
|
for (int i = 0; i < titleMetadata.ContentCount; i++)
|
|
{
|
|
titleMetadata.ContentChunkRecords[i] = ParseContentChunkRecord(data);
|
|
}
|
|
|
|
// Certificates only exist in standalone TMD files
|
|
if (fromCdn)
|
|
{
|
|
titleMetadata.CertificateChain = new Certificate[2];
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
var certificate = ParseCertificate(data);
|
|
if (certificate == null)
|
|
return null;
|
|
|
|
titleMetadata.CertificateChain[i] = certificate;
|
|
}
|
|
}
|
|
|
|
return titleMetadata;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a content info record
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled content info record on success, null on error</returns>
|
|
private static ContentInfoRecord ParseContentInfoRecord(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
ContentInfoRecord contentInfoRecord = new ContentInfoRecord();
|
|
|
|
contentInfoRecord.ContentIndexOffset = data.ReadUInt16();
|
|
contentInfoRecord.ContentCommandCount = data.ReadUInt16();
|
|
contentInfoRecord.UnhashedContentRecordsSHA256Hash = data.ReadBytes(0x20);
|
|
|
|
return contentInfoRecord;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a content chunk record
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled content chunk record on success, null on error</returns>
|
|
private static ContentChunkRecord ParseContentChunkRecord(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
ContentChunkRecord contentChunkRecord = new ContentChunkRecord();
|
|
|
|
contentChunkRecord.ContentId = data.ReadUInt32();
|
|
contentChunkRecord.ContentIndex = (ContentIndex)data.ReadUInt16();
|
|
contentChunkRecord.ContentType = (TMDContentType)data.ReadUInt16();
|
|
contentChunkRecord.ContentSize = data.ReadUInt64();
|
|
contentChunkRecord.SHA256Hash = data.ReadBytes(0x20);
|
|
|
|
return contentChunkRecord;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Stream into a meta data
|
|
/// </summary>
|
|
/// <param name="data">Stream to parse</param>
|
|
/// <returns>Filled meta data on success, null on error</returns>
|
|
private static MetaData ParseMetaData(Stream data)
|
|
{
|
|
// TODO: Use marshalling here instead of building
|
|
MetaData metaData = new MetaData();
|
|
|
|
metaData.TitleIDDependencyList = data.ReadBytes(0x180);
|
|
metaData.Reserved1 = data.ReadBytes(0x180);
|
|
metaData.CoreVersion = data.ReadUInt32();
|
|
metaData.Reserved2 = data.ReadBytes(0xFC);
|
|
metaData.IconData = data.ReadBytes(0x36C0);
|
|
|
|
return metaData;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|