diff --git a/src/LibHac/Fs/QueryRangeInfo.cs b/src/LibHac/Fs/QueryRangeInfo.cs
index d44cfbf8..3be7c655 100644
--- a/src/LibHac/Fs/QueryRangeInfo.cs
+++ b/src/LibHac/Fs/QueryRangeInfo.cs
@@ -19,12 +19,12 @@ public struct QueryRangeInfo
AesCtrKeyType |= other.AesCtrKeyType;
SpeedEmulationType |= other.SpeedEmulationType;
}
+}
- [Flags]
- public enum AesCtrKeyTypeFlag
- {
- InternalKeyForSoftwareAes = 1 << 0,
- InternalKeyForHardwareAes = 1 << 1,
- ExternalKeyForHardwareAes = 1 << 2
- }
+[Flags]
+public enum AesCtrKeyTypeFlag
+{
+ InternalKeyForSoftwareAes = 1 << 0,
+ InternalKeyForHardwareAes = 1 << 1,
+ ExternalKeyForHardwareAes = 1 << 2
}
\ No newline at end of file
diff --git a/src/LibHac/FsSystem/AesCtrCounterExtendedStorage.cs b/src/LibHac/FsSystem/AesCtrCounterExtendedStorage.cs
index 45d0cddc..f9449a51 100644
--- a/src/LibHac/FsSystem/AesCtrCounterExtendedStorage.cs
+++ b/src/LibHac/FsSystem/AesCtrCounterExtendedStorage.cs
@@ -404,8 +404,8 @@ public class AesCtrCounterExtendedStorage : IStorage
Unsafe.SkipInit(out QueryRangeInfo info);
info.Clear();
info.AesCtrKeyType = (int)(_decryptor.Get.HasExternalDecryptionKey()
- ? QueryRangeInfo.AesCtrKeyTypeFlag.ExternalKeyForHardwareAes
- : QueryRangeInfo.AesCtrKeyTypeFlag.InternalKeyForHardwareAes);
+ ? AesCtrKeyTypeFlag.ExternalKeyForHardwareAes
+ : AesCtrKeyTypeFlag.InternalKeyForHardwareAes);
outInfo.Merge(in info);
diff --git a/src/LibHac/FsSystem/AesCtrStorage.cs b/src/LibHac/FsSystem/AesCtrStorage.cs
index 5fed0307..13eb5b22 100644
--- a/src/LibHac/FsSystem/AesCtrStorage.cs
+++ b/src/LibHac/FsSystem/AesCtrStorage.cs
@@ -215,7 +215,7 @@ public class AesCtrStorage : IStorage
Unsafe.SkipInit(out QueryRangeInfo info);
info.Clear();
- info.AesCtrKeyType = (int)QueryRangeInfo.AesCtrKeyTypeFlag.InternalKeyForSoftwareAes;
+ info.AesCtrKeyType = (int)AesCtrKeyTypeFlag.InternalKeyForSoftwareAes;
outInfo.Merge(in info);
diff --git a/src/LibHac/FsSystem/BucketTree.cs b/src/LibHac/FsSystem/BucketTree.cs
index f18e619a..c454f602 100644
--- a/src/LibHac/FsSystem/BucketTree.cs
+++ b/src/LibHac/FsSystem/BucketTree.cs
@@ -128,7 +128,7 @@ file struct StorageNode
/// Based on nnSdk 16.2.0 (FS 16.0.0)
public partial class BucketTree : IDisposable
{
- private const uint Signature = 0x52544B42; // BKTR
+ public const uint Signature = 0x52544B42; // BKTR
private const int MaxVersion = 1;
private const int NodeSizeMin = 1024;
diff --git a/src/LibHac/FsSystem/CompressionCommon.cs b/src/LibHac/FsSystem/CompressionCommon.cs
index e0d77183..8ed4b7ef 100644
--- a/src/LibHac/FsSystem/CompressionCommon.cs
+++ b/src/LibHac/FsSystem/CompressionCommon.cs
@@ -10,7 +10,14 @@ public enum CompressionType : byte
Unknown = 4
}
-public delegate Result DecompressorFunction(Span destination, ReadOnlySpan source);
+public ref struct DecompressionTask
+{
+ public Span Destination;
+ public ReadOnlySpan Source;
+}
+
+public delegate Result DecompressorFunction(DecompressionTask task);
+
public delegate DecompressorFunction GetDecompressorFunction(CompressionType compressionType);
public static class CompressionTypeUtility
diff --git a/src/LibHac/FsSystem/IAesCtrDecryptor.cs b/src/LibHac/FsSystem/IAesCtrDecryptor.cs
new file mode 100644
index 00000000..b1a1709a
--- /dev/null
+++ b/src/LibHac/FsSystem/IAesCtrDecryptor.cs
@@ -0,0 +1,13 @@
+using System;
+using LibHac.Fs;
+using LibHac.Spl;
+
+namespace LibHac.FsSystem;
+
+public interface IAesCtrDecryptor : IDisposable
+{
+ Result Decrypt(Span destination, ReadOnlySpan iv, ReadOnlySpan source);
+ void PrioritizeSw();
+ void SetExternalKeySource(in Spl.AccessKey keySource);
+ AesCtrKeyTypeFlag GetKeyTypeFlag();
+}
\ No newline at end of file
diff --git a/src/LibHac/FsSystem/NcaHeader.cs b/src/LibHac/FsSystem/NcaHeader.cs
index 4efacc51..9b696052 100644
--- a/src/LibHac/FsSystem/NcaHeader.cs
+++ b/src/LibHac/FsSystem/NcaHeader.cs
@@ -1,12 +1,51 @@
using System;
+using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using LibHac.Common;
using LibHac.Common.FixedArrays;
+using LibHac.Crypto;
using LibHac.Fs;
+using LibHac.Util;
+using static LibHac.FsSystem.Anonymous;
namespace LibHac.FsSystem;
+file static class Anonymous
+{
+ public static bool IsZero(ReadOnlySpan value) where T : unmanaged
+ {
+ ReadOnlySpan valueBytes = MemoryMarshal.Cast(value);
+ Span zero = stackalloc byte[valueBytes.Length];
+ zero.Clear();
+
+ return CryptoUtil.IsSameBytes(valueBytes, zero, valueBytes.Length);
+ }
+
+ public static bool IsZero(in T value) where T : unmanaged
+ {
+ ReadOnlySpan valueBytes = SpanHelpers.AsReadOnlyByteSpan(in value);
+ Span zero = stackalloc byte[Unsafe.SizeOf()];
+ zero.Clear();
+
+ return CryptoUtil.IsSameBytes(valueBytes, zero, Unsafe.SizeOf());
+ }
+
+ public static bool IsZero(in T value, int startOffset) where T : unmanaged
+ {
+ ReadOnlySpan valueBytes = SpanHelpers.AsReadOnlyByteSpan(in value).Slice(startOffset);
+ Span zero = stackalloc byte[Unsafe.SizeOf() - startOffset];
+ zero.Clear();
+
+ return CryptoUtil.IsSameBytes(valueBytes, zero, Unsafe.SizeOf() - startOffset);
+ }
+
+ public static bool IsIncluded(T value, T min, T max) where T : IComparisonOperators
+ {
+ return min <= value && value <= max;
+ }
+}
+
///
/// The structure used as the header for an NCA file.
///
@@ -96,6 +135,76 @@ public struct NcaHeader
public static uint ByteToSector(ulong byteIndex) => (uint)(byteIndex >> SectorShift);
public readonly byte GetProperKeyGeneration() => Math.Max(KeyGeneration1, KeyGeneration2);
+
+ public readonly Result Verify()
+ {
+ const uint magicBodyMask = 0xFFFFFF;
+ const uint magicVersionMask = 0xFF000000;
+ const uint magicBodyValue = 0x41434E; // NCA
+ const uint magicVersionMax = 0x33000000; // \0\0\03
+
+ if ((Magic & magicBodyMask) != magicBodyValue || (Magic & magicVersionMask) > magicVersionMax)
+ return ResultFs.InvalidNcaHeader.Log();
+
+ if (!IsIncluded((int)DistributionTypeValue, (int)DistributionType.Download, (int)DistributionType.GameCard))
+ return ResultFs.InvalidNcaHeader.Log();
+
+ if (!IsIncluded((int)ContentTypeValue, (int)ContentType.Program, (int)ContentType.PublicData))
+ return ResultFs.InvalidNcaHeader.Log();
+
+ if (KeyAreaEncryptionKeyIndex >= NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount &&
+ KeyAreaEncryptionKeyIndex != NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexZeroKey)
+ {
+ return ResultFs.InvalidNcaHeader.Log();
+ }
+
+ if (ProgramId == 0)
+ return ResultFs.InvalidNcaHeader.Log();
+
+ if (SdkAddonVersion == 0)
+ return ResultFs.InvalidNcaHeader.Log();
+
+ if (!IsZero(in Reserved222))
+ return ResultFs.InvalidNcaHeader.Log();
+
+ if (!IsZero(in Reserved224))
+ return ResultFs.InvalidNcaHeader.Log();
+
+ long es = long.MaxValue;
+
+ for (int i = 0; i < FsCountMax; i++)
+ {
+ if (FsInfos[i].StartSector != 0 || FsInfos[i].EndSector != 0)
+ {
+ if (es == long.MaxValue)
+ es = FsInfos[i].EndSector;
+
+ if (es < FsInfos[i].EndSector || FsInfos[i].StartSector >= FsInfos[i].EndSector)
+ return ResultFs.InvalidNcaHeader.Log();
+
+ es = FsInfos[i].StartSector;
+
+ if (FsInfos[i].HashSectors != ByteToSector(0x200))
+ return ResultFs.InvalidNcaHeader.Log();
+ }
+ else if (FsInfos[i].HashSectors != 0)
+ {
+ return ResultFs.InvalidNcaHeader.Log();
+ }
+
+ if (FsInfos[i].Reserved != 0)
+ return ResultFs.InvalidNcaHeader.Log();
+ }
+
+ const int offset = (int)DecryptionKey.Count * Aes.KeySize128;
+ if (!IsZero(EncryptedKeys[..][offset..]))
+ return ResultFs.InvalidNcaHeader.Log();
+
+ if (!IsZero(EncryptedKeys[offset..]))
+ return ResultFs.InvalidNcaHeader.Log();
+
+ return Result.Success;
+ }
}
public struct NcaPatchInfo
@@ -107,8 +216,15 @@ public struct NcaPatchInfo
public long AesCtrExSize;
public Array16 AesCtrExHeader;
- public readonly bool HasIndirectTable() => IndirectSize != 0;
- public readonly bool HasAesCtrExTable() => AesCtrExSize != 0;
+ public readonly bool HasIndirectTable()
+ {
+ return Unsafe.As, uint>(ref Unsafe.AsRef(in IndirectHeader)) == BucketTree.Signature;
+ }
+
+ public readonly bool HasAesCtrExTable()
+ {
+ return Unsafe.As, uint>(ref Unsafe.AsRef(in AesCtrExHeader)) == BucketTree.Signature;
+ }
}
public struct NcaSparseInfo
@@ -129,6 +245,11 @@ public struct NcaSparseInfo
sparseUpperIv.Generation = GetGeneration();
return sparseUpperIv;
}
+
+ public readonly bool HasSparseTable()
+ {
+ return Unsafe.As, uint>(ref Unsafe.AsRef(in MetaHeader)) == BucketTree.Signature;
+ }
}
public struct NcaCompressionInfo
@@ -219,7 +340,8 @@ public struct NcaFsHeader
public enum MetaDataHashType : byte
{
None = 0,
- HierarchicalIntegrity = 1
+ HierarchicalIntegrity = 1,
+ HierarchicalIntegritySha3 = 2
}
public struct Region
@@ -240,10 +362,45 @@ public struct NcaFsHeader
public int BlockSize;
public int LayerCount;
public Array5 LayerRegions;
+
+ public readonly Result Verify()
+ {
+ if (IsZero(in MasterHash))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (BlockSize <= 0 || !BitUtil.IsPowerOfTwo(BlockSize))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (LayerCount != 2)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ long currentOffset = 0;
+
+ for (int i = 0; i < LayerCount; i++)
+ {
+ if (currentOffset > LayerRegions[i].Offset)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (LayerRegions[i].Size <= 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ currentOffset = LayerRegions[i].Offset + LayerRegions[i].Size;
+ }
+
+ for (int i = LayerCount; i < 5; i++)
+ {
+ if (!IsZero(in LayerRegions[i]))
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+
+ return Result.Success;
+ }
}
public struct IntegrityMetaInfo
{
+ public const int HashSize = Sha256Generator.HashSize;
+
public uint Magic;
public uint Version;
public uint MasterHashSize;
@@ -269,9 +426,194 @@ public struct NcaFsHeader
public Array32 Value;
}
}
+
+ public readonly Result Verify()
+ {
+ if (Magic != HierarchicalIntegrityVerificationStorage.IntegrityVerificationStorageMagic)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (Version != HierarchicalIntegrityVerificationStorage.IntegrityVerificationStorageVersion)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (MasterHashSize != HashSize)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (!IsIncluded(LevelHashInfo.MaxLayers, Constants.IntegrityMinLayerCount, Constants.IntegrityMaxLayerCount))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ long currentOffset = 0;
+
+ for (int i = 0; i < LevelHashInfo.MaxLayers - 1; i++)
+ {
+ ref readonly InfoLevelHash.HierarchicalIntegrityVerificationLevelInformation layer = ref LevelHashInfo.Layers[i];
+
+ if (layer.OrderBlock <= 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (currentOffset > layer.Offset || !Alignment.IsAligned(layer.Offset.Get(), (ulong)(1 << layer.OrderBlock)))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (layer.Size <= 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (!IsZero(layer.Reserved))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ currentOffset = layer.Offset + layer.Size;
+ }
+
+ for (int i = LevelHashInfo.MaxLayers - 1; i < 6; i++)
+ {
+ if (!IsZero(in LevelHashInfo.Layers[i]))
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+
+ return Result.Success;
+ }
}
}
+ public Result Verify()
+ {
+ if (Version != 2)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (!IsIncluded((int)FsTypeValue, (int)FsType.RomFs, (int)FsType.PartitionFs))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (!IsIncluded((int)HashTypeValue, (int)HashType.None, (int)HashType.HierarchicalIntegritySha3Hash) || HashTypeValue == HashType.AutoSha3)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (!IsIncluded((int)EncryptionTypeValue, (int)EncryptionType.None, (int)EncryptionType.AesCtrExSkipLayerHash))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (!IsIncluded((int)MetaDataHashTypeValue, (int)MetaDataHashType.None, (int)MetaDataHashType.HierarchicalIntegritySha3))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (!IsZero(in Reserved))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ switch (HashTypeValue)
+ {
+ case HashType.HierarchicalSha256Hash:
+ case HashType.HierarchicalSha3256Hash:
+ {
+ Result res = HashDataValue.HierarchicalSha256.Verify();
+ if (res.IsFailure()) return res.Miss();
+
+ if (!IsZero(in HashDataValue, Unsafe.SizeOf()))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ break;
+ }
+ case HashType.HierarchicalIntegrityHash:
+ case HashType.HierarchicalIntegritySha3Hash:
+ {
+ Result res = HashDataValue.IntegrityMeta.Verify();
+ if (res.IsFailure()) return res.Miss();
+
+ if (!IsZero(in HashDataValue, Unsafe.SizeOf()))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ break;
+ }
+ default:
+ {
+ if (!IsZero(in HashDataValue))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ break;
+ }
+ }
+
+ if (EncryptionTypeValue == EncryptionType.AesCtrEx || EncryptionTypeValue == EncryptionType.AesCtrExSkipLayerHash)
+ {
+ if (PatchInfo.IndirectOffset < 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (PatchInfo.IndirectSize <= 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (IsZero(in PatchInfo.IndirectHeader))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (PatchInfo.AesCtrExOffset < 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (MetaDataHashTypeValue == MetaDataHashType.None)
+ {
+ if (PatchInfo.AesCtrExSize <= 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+ else if (PatchInfo.IndirectOffset == 0)
+ {
+ if (PatchInfo.AesCtrExSize != 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+ else if (PatchInfo.AesCtrExSize <= 0)
+ {
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+
+ if (IsZero(in PatchInfo.AesCtrExHeader))
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+ else if (EncryptionTypeValue != EncryptionType.None && !IsZero(in PatchInfo))
+ {
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+
+ if (EncryptionTypeValue != EncryptionType.AesCtr
+ && EncryptionTypeValue != EncryptionType.AesCtrEx
+ && EncryptionTypeValue != EncryptionType.AesCtrSkipLayerHash
+ && EncryptionTypeValue != EncryptionType.AesCtrExSkipLayerHash
+ && !IsZero(in AesCtrUpperIv))
+ {
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+
+ if (SparseInfo.Generation != 0)
+ {
+ if (SparseInfo.MetaOffset < 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (SparseInfo.MetaSize < 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (SparseInfo.PhysicalOffset < 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (!IsZero(SparseInfo.Reserved))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ var header = SpanHelpers.AsStruct(SparseInfo.MetaHeader);
+ Result res = header.Verify();
+ if (res.IsFailure()) return res.Miss();
+ }
+ else if (!IsZero(in SparseInfo))
+ {
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+
+ if (MetaDataHashTypeValue != MetaDataHashType.None)
+ {
+ if (MetaDataHashDataInfo.Offset <= 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ if (MetaDataHashDataInfo.Size <= 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+ else if (!IsZero(MetaDataHashDataInfo))
+ {
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+
+ if (!IsZero(Padding))
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ return Result.Success;
+ }
+
public readonly Result GetHashTargetOffset(out long outOffset)
{
UnsafeHelpers.SkipParamInit(out outOffset);
diff --git a/src/LibHac/FsSystem/NcaReader17.cs b/src/LibHac/FsSystem/NcaReader17.cs
new file mode 100644
index 00000000..b04ecd1a
--- /dev/null
+++ b/src/LibHac/FsSystem/NcaReader17.cs
@@ -0,0 +1,454 @@
+using System;
+using System.Runtime.CompilerServices;
+using LibHac.Common;
+using LibHac.Crypto;
+using LibHac.Diag;
+using LibHac.Fs;
+using LibHac.Spl;
+
+namespace LibHac.FsSystem;
+
+///
+/// Handles reading information from an NCA's header.
+///
+/// Based on nnSdk 17.5.0 (FS 17.0.0)
+public class NcaReader17 : IDisposable
+{
+ private RuntimeNcaHeader _header;
+ private SharedRef _bodyStorage;
+ private SharedRef _headerStorage;
+ private SharedRef _aesCtrDecryptor;
+ private GetDecompressorFunction _getDecompressorFunc;
+ private IHash256GeneratorFactorySelector _hashGeneratorFactorySelector;
+
+ public NcaReader17(in RuntimeNcaHeader runtimeNcaHeader, ref readonly SharedRef notVerifiedHeaderStorage,
+ ref readonly SharedRef bodyStorage, ref readonly SharedRef aesCtrDecryptor,
+ in NcaCompressionConfiguration compressionConfig, IHash256GeneratorFactorySelector hashGeneratorFactorySelector)
+ {
+ Assert.SdkRequiresNotNull(in notVerifiedHeaderStorage);
+ Assert.SdkRequiresNotNull(in bodyStorage);
+ Assert.SdkRequiresNotNull(hashGeneratorFactorySelector);
+
+ _header = runtimeNcaHeader;
+
+ _headerStorage = SharedRef.CreateCopy(in notVerifiedHeaderStorage);
+ _bodyStorage = SharedRef.CreateCopy(in bodyStorage);
+ _aesCtrDecryptor = SharedRef.CreateCopy(in aesCtrDecryptor);
+
+ _getDecompressorFunc = compressionConfig.GetDecompressorFunc;
+ _hashGeneratorFactorySelector = hashGeneratorFactorySelector;
+ }
+
+ public void Dispose()
+ {
+ _bodyStorage.Destroy();
+ _headerStorage.Destroy();
+ _aesCtrDecryptor.Destroy();
+ }
+
+ public Result ReadHeader(out NcaFsHeader outHeader, int index)
+ {
+ UnsafeHelpers.SkipParamInit(out outHeader);
+
+ Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax);
+
+ long offset = _header.FsHeadersOffset + Unsafe.SizeOf() * (long)index;
+ return _headerStorage.Get.Read(offset, SpanHelpers.AsByteSpan(ref outHeader)).Ret();
+ }
+
+ public Result GetHeaderSign2(Span outBuffer)
+ {
+ Assert.SdkRequiresGreaterEqual((uint)outBuffer.Length, _header.Header2SignInfo.Size);
+
+ return _headerStorage.Get
+ .Read(_header.Header2SignInfo.Size, outBuffer.Slice(0, (int)_header.Header2SignInfo.Size)).Ret();
+ }
+
+ public void GetHeaderSign2TargetHash(Span outBuffer)
+ {
+ Assert.SdkRequiresEqual(outBuffer.Length, Unsafe.SizeOf());
+
+ _header.Header2SignInfo.Hash.Value[..].CopyTo(outBuffer);
+ }
+
+ public SharedRef GetSharedBodyStorage()
+ {
+ Assert.SdkRequiresNotNull(_bodyStorage);
+
+ return SharedRef.CreateCopy(in _bodyStorage);
+ }
+
+ public NcaHeader.DistributionType GetDistributionType()
+ {
+ return _header.DistributionType;
+ }
+
+ public NcaHeader.ContentType GetContentType()
+ {
+ return _header.ContentType;
+ }
+
+ public byte GetKeyGeneration()
+ {
+ return _header.KeyGeneration;
+ }
+
+ public ulong GetProgramId()
+ {
+ return _header.ProgramId;
+ }
+
+ public void GetRightsId(Span outBuffer)
+ {
+ Assert.SdkRequiresGreaterEqual(outBuffer.Length, NcaHeader.RightsIdSize);
+
+ _header.RightsId[..].CopyTo(outBuffer);
+ }
+
+ public bool HasFsInfo(int index)
+ {
+ Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax);
+ return _header.FsInfos[index].StartSector != 0 || _header.FsInfos[index].EndSector != 0;
+ }
+
+ public void GetFsHeaderHash(out Hash outHash, int index)
+ {
+ Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax);
+ outHash = _header.FsInfos[index].Hash;
+ }
+
+ public void GetFsInfo(out NcaHeader.FsInfo outFsInfo, int index)
+ {
+ Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax);
+
+ outFsInfo = new NcaHeader.FsInfo
+ {
+ StartSector = _header.FsInfos[index].StartSector,
+ EndSector = _header.FsInfos[index].EndSector,
+ HashSectors = _header.FsInfos[index].HashSectors,
+ Reserved = 0
+ };
+ }
+
+ public ulong GetFsOffset(int index)
+ {
+ Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax);
+
+ return NcaHeader.SectorToByte(_header.FsInfos[index].StartSector);
+ }
+
+ public ulong GetFsEndOffset(int index)
+ {
+ Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax);
+
+ return NcaHeader.SectorToByte(_header.FsInfos[index].EndSector);
+ }
+
+ public ulong GetFsSize(int index)
+ {
+ Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax);
+
+ return NcaHeader.SectorToByte(_header.FsInfos[index].EndSector - _header.FsInfos[index].StartSector);
+ }
+
+ public void PrioritizeSwAes()
+ {
+ if (_aesCtrDecryptor.HasValue)
+ {
+ _aesCtrDecryptor.Get.PrioritizeSw();
+ }
+ }
+
+ public void SetExternalDecryptionKey(in AccessKey keySource)
+ {
+ if (_aesCtrDecryptor.HasValue)
+ {
+ _aesCtrDecryptor.Get.SetExternalKeySource(in keySource);
+ }
+ }
+
+ public RuntimeNcaHeader GetHeader()
+ {
+ return _header;
+ }
+
+ public SharedRef GetDecryptor()
+ {
+ return SharedRef.CreateCopy(in _aesCtrDecryptor);
+ }
+
+ public GetDecompressorFunction GetDecompressor()
+ {
+ Assert.SdkRequiresNotNull(_getDecompressorFunc);
+ return _getDecompressorFunc;
+ }
+
+ public IHash256GeneratorFactorySelector GetHashGeneratorFactorySelector()
+ {
+ Assert.SdkRequiresNotNull(_hashGeneratorFactorySelector);
+ return _hashGeneratorFactorySelector;
+ }
+
+ public Result Verify()
+ {
+ Assert.SdkRequiresNotNull(_bodyStorage);
+
+ for (int fsIndex = 0; fsIndex < NcaHeader.FsCountMax; fsIndex++)
+ {
+ var reader = new NcaFsHeaderReader17();
+ if (HasFsInfo(fsIndex))
+ {
+ Result res = reader.Initialize(this, fsIndex);
+ if (res.IsFailure()) return res.Miss();
+
+ res = reader.Verify(_header.ContentType);
+ if (res.IsFailure()) return res.Miss();
+ }
+ else
+ {
+ Result res = ReadHeader(out NcaFsHeader header, fsIndex);
+ if (res.IsFailure()) return res.Miss();
+
+ NcaFsHeader zero = default;
+ if (!CryptoUtil.IsSameBytes(SpanHelpers.AsReadOnlyByteSpan(in header),
+ SpanHelpers.AsReadOnlyByteSpan(in zero), Unsafe.SizeOf()))
+ {
+ return ResultFs.InvalidNcaFsHeader.Log();
+ }
+ }
+ }
+
+ return Result.Success;
+ }
+}
+
+///
+/// Handles reading information from the of a file system inside an NCA file.
+///
+/// Based on nnSdk 17.5.0 (FS 17.0.0)
+public class NcaFsHeaderReader17
+{
+ private NcaFsHeader _header;
+ private int _fsIndex;
+
+ public NcaFsHeaderReader17()
+ {
+ _fsIndex = -1;
+ }
+
+ public bool IsInitialized()
+ {
+ return _fsIndex >= 0;
+ }
+
+ public Result Initialize(NcaReader17 reader, int index)
+ {
+ _fsIndex = -1;
+
+ Result res = reader.ReadHeader(out _header, index);
+ if (res.IsFailure()) return res.Miss();
+
+ Unsafe.SkipInit(out Hash hash);
+ IHash256GeneratorFactory generator = reader.GetHashGeneratorFactorySelector().GetFactory(HashAlgorithmType.Sha2);
+ generator.GenerateHash(hash.Value, SpanHelpers.AsReadOnlyByteSpan(in _header));
+
+ reader.GetFsHeaderHash(out Hash fsHeaderHash, index);
+
+ if (!CryptoUtil.IsSameBytes(fsHeaderHash.Value, hash.Value, Unsafe.SizeOf()))
+ {
+ return ResultFs.NcaFsHeaderHashVerificationFailed.Log();
+ }
+
+ _fsIndex = index;
+ return Result.Success;
+ }
+
+ public ref readonly NcaFsHeader.HashData GetHashData()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return ref _header.HashDataValue;
+ }
+
+ public ushort GetVersion()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.Version;
+ }
+
+ public int GetFsIndex()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _fsIndex;
+ }
+
+ public NcaFsHeader.FsType GetFsType()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.FsTypeValue;
+ }
+
+ public NcaFsHeader.HashType GetHashType()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.HashTypeValue;
+ }
+
+ public NcaFsHeader.EncryptionType GetEncryptionType()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.EncryptionTypeValue;
+ }
+
+ public NcaFsHeader.MetaDataHashType GetPatchMetaHashType()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.MetaDataHashTypeValue;
+ }
+
+ public NcaFsHeader.MetaDataHashType GetSparseMetaHashType()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.MetaDataHashTypeValue;
+ }
+
+ public Result GetHashTargetOffset(out long outOffset)
+ {
+ Assert.SdkRequires(IsInitialized());
+
+ Result res = _header.GetHashTargetOffset(out outOffset);
+ if (res.IsFailure()) return res.Miss();
+
+ return Result.Success;
+ }
+
+ public bool IsSkipLayerHashEncryption()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.IsSkipLayerHashEncryption();
+ }
+
+ public ref readonly NcaPatchInfo GetPatchInfo()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return ref _header.PatchInfo;
+ }
+
+ public NcaAesCtrUpperIv GetAesCtrUpperIv()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.AesCtrUpperIv;
+ }
+
+ public bool ExistsSparseLayer()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.SparseInfo.Generation != 0;
+ }
+
+ public ref readonly NcaSparseInfo GetSparseInfo()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return ref _header.SparseInfo;
+ }
+
+ public bool ExistsCompressionLayer()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.CompressionInfo.TableOffset != 0 && _header.CompressionInfo.TableSize != 0;
+ }
+
+ public ref readonly NcaCompressionInfo GetCompressionInfo()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return ref _header.CompressionInfo;
+ }
+
+ public bool ExistsPatchMetaHashLayer()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.MetaDataHashDataInfo.Size != 0 && GetPatchInfo().HasIndirectTable();
+ }
+
+ public bool ExistsSparseMetaHashLayer()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return _header.MetaDataHashDataInfo.Size != 0 && ExistsSparseLayer();
+ }
+
+ public ref readonly NcaMetaDataHashDataInfo GetPatchMetaDataHashDataInfo()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return ref _header.MetaDataHashDataInfo;
+ }
+
+ public ref readonly NcaMetaDataHashDataInfo GetSparseMetaDataHashDataInfo()
+ {
+ Assert.SdkRequires(IsInitialized());
+ return ref _header.MetaDataHashDataInfo;
+ }
+
+ public void GetRawData(Span outBuffer)
+ {
+ Assert.SdkRequires(IsInitialized());
+ Assert.SdkRequiresLessEqual(Unsafe.SizeOf(), outBuffer.Length);
+
+ SpanHelpers.AsReadOnlyByteSpan(in _header).CopyTo(outBuffer);
+ }
+
+ public Result Verify(NcaHeader.ContentType contentType)
+ {
+ Assert.SdkRequires(IsInitialized());
+ Assert.SdkRequiresWithinMinMax((int)contentType, (int)NcaHeader.ContentType.Program, (int)NcaHeader.ContentType.PublicData);
+
+ Result res = _header.Verify();
+ if (res.IsFailure()) return res.Miss();
+
+ const uint programSecureValue = 1;
+ const uint dataSecureValue = 2;
+ const uint htmlDocumentSecureValue = 4;
+ const uint legalInformationSecureValue = 5;
+
+ // Mask out the program index part of the secure value
+ uint secureValue = _header.AesCtrUpperIv.SecureValue & 0xFFFFFF;
+
+ if (GetEncryptionType() == NcaFsHeader.EncryptionType.None)
+ {
+ if (secureValue != 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+
+ return Result.Success;
+ }
+
+ switch (contentType)
+ {
+ case NcaHeader.ContentType.Program:
+ switch (_fsIndex)
+ {
+ case 0:
+ if (secureValue != programSecureValue)
+ return ResultFs.InvalidNcaFsHeader.Log();
+ break;
+ case 1:
+ if (secureValue != dataSecureValue)
+ return ResultFs.InvalidNcaFsHeader.Log();
+ break;
+ default:
+ if (secureValue != 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+ break;
+ }
+
+ break;
+ case NcaHeader.ContentType.Manual:
+ if (secureValue != htmlDocumentSecureValue && secureValue != legalInformationSecureValue)
+ return ResultFs.InvalidNcaFsHeader.Log();
+ break;
+ default:
+ if (secureValue != 0)
+ return ResultFs.InvalidNcaFsHeader.Log();
+ break;
+ }
+
+ return Result.Success;
+ }
+}
\ No newline at end of file
diff --git a/src/LibHac/FsSystem/RuntimeNcaHeader.cs b/src/LibHac/FsSystem/RuntimeNcaHeader.cs
new file mode 100644
index 00000000..c07021a0
--- /dev/null
+++ b/src/LibHac/FsSystem/RuntimeNcaHeader.cs
@@ -0,0 +1,49 @@
+using System;
+using LibHac.Common.FixedArrays;
+
+namespace LibHac.FsSystem;
+
+public struct RuntimeKeySourceInfo
+{
+ public uint Offset;
+ public uint Size;
+ public Hash Hash;
+}
+
+public struct RuntimeNcaHeader
+{
+ public struct FsInfo
+ {
+ public uint StartSector;
+ public uint EndSector;
+ public uint HashSectors;
+ public Hash Hash;
+ }
+
+ public struct SignInfo
+ {
+ public uint Offset;
+ public uint Size;
+ public Hash Hash;
+ }
+
+ public NcaHeader.DistributionType DistributionType;
+ public NcaHeader.ContentType ContentType;
+ public byte KeyGeneration;
+ public ulong ProgramId;
+ public Array16 RightsId;
+ public uint FsHeadersOffset;
+ public Array4 FsInfos;
+ public SignInfo Header2SignInfo;
+ public RuntimeKeySourceInfo KeySourceInfo;
+
+ public static Result CheckUnsupportedVersion(uint magicValue)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Result InitializeCommonForV3(in NcaHeader header, IHash256GeneratorFactorySelector hashGeneratorFactorySelector)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file