diff --git a/src/LibHac/Fs/MemoryStorage.cs b/src/LibHac/Fs/MemoryStorage.cs index 60c2e7f9..ed43bd8c 100644 --- a/src/LibHac/Fs/MemoryStorage.cs +++ b/src/LibHac/Fs/MemoryStorage.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using LibHac.Diag; namespace LibHac.Fs; @@ -10,11 +11,25 @@ namespace LibHac.Fs; /// Based on nnSdk 14.3.0 (FS 14.1.0) public class MemoryStorage : IStorage { - private byte[] _storageBuffer; + private byte[] _buffer; + private int _size; public MemoryStorage(byte[] buffer) { - _storageBuffer = buffer; + _buffer = buffer; + _size = buffer.Length; + } + + public MemoryStorage(byte[] buffer, int size) + { + Assert.SdkRequiresNotNull(buffer); + Assert.SdkRequiresInRange(size, 0, buffer.Length); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + Abort.DoAbortUnless(buffer is null || 0 <= size && size < buffer.Length); + + _buffer = buffer; + _size = size; } public override Result Read(long offset, Span destination) @@ -22,10 +37,10 @@ public class MemoryStorage : IStorage if (destination.Length == 0) return Result.Success; - Result res = CheckAccessRange(offset, destination.Length, _storageBuffer.Length); + Result res = CheckAccessRange(offset, destination.Length, _size); if (res.IsFailure()) return res.Miss(); - _storageBuffer.AsSpan((int)offset, destination.Length).CopyTo(destination); + _buffer.AsSpan((int)offset, destination.Length).CopyTo(destination); return Result.Success; } @@ -35,10 +50,10 @@ public class MemoryStorage : IStorage if (source.Length == 0) return Result.Success; - Result res = CheckAccessRange(offset, source.Length, _storageBuffer.Length); + Result res = CheckAccessRange(offset, source.Length, _size); if (res.IsFailure()) return res.Miss(); - source.CopyTo(_storageBuffer.AsSpan((int)offset)); + source.CopyTo(_buffer.AsSpan((int)offset)); return Result.Success; } @@ -55,7 +70,7 @@ public class MemoryStorage : IStorage public override Result GetSize(out long size) { - size = _storageBuffer.Length; + size = _size; return Result.Success; } diff --git a/src/LibHac/FsSrv/FsCreator/EmulatedGameCardFsCreator.cs b/src/LibHac/FsSrv/FsCreator/EmulatedGameCardFsCreator.cs index f5200e97..8ba530a1 100644 --- a/src/LibHac/FsSrv/FsCreator/EmulatedGameCardFsCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/EmulatedGameCardFsCreator.cs @@ -17,6 +17,8 @@ public class EmulatedGameCardFsCreator : IGameCardFileSystemCreator _gameCard = gameCard; } + public void Dispose() { } + public Result Create(ref SharedRef outFileSystem, GameCardHandle handle, GameCardPartition partitionType) { diff --git a/src/LibHac/FsSrv/FsCreator/EmulatedGameCardStorageCreator.cs b/src/LibHac/FsSrv/FsCreator/EmulatedGameCardStorageCreator.cs index 51ce7036..440ffd18 100644 --- a/src/LibHac/FsSrv/FsCreator/EmulatedGameCardStorageCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/EmulatedGameCardStorageCreator.cs @@ -13,6 +13,8 @@ public class EmulatedGameCardStorageCreator : IGameCardStorageCreator GameCard = gameCard; } + public void Dispose() { } + public Result CreateReadOnly(GameCardHandle handle, ref SharedRef outStorage) { if (GameCard.IsGameCardHandleInvalid(handle)) diff --git a/src/LibHac/FsSrv/FsCreator/GameCardFileSystemCreator.cs b/src/LibHac/FsSrv/FsCreator/GameCardFileSystemCreator.cs new file mode 100644 index 00000000..75749c63 --- /dev/null +++ b/src/LibHac/FsSrv/FsCreator/GameCardFileSystemCreator.cs @@ -0,0 +1,343 @@ +using System; +using System.Buffers; +using LibHac.Common; +using LibHac.Diag; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSrv.Storage; +using LibHac.FsSystem; +using LibHac.Gc; +using LibHac.Os; +using LibHac.Util; +using PartitionEntry = LibHac.FsSystem.Impl.Sha256PartitionFileSystemFormat.PartitionEntry; + +namespace LibHac.FsSrv.FsCreator; + +/// +/// Reads the root partition of a game card and handles opening the various partitions it contains. +/// +/// Based on nnSdk 15.3.0 (FS 15.0.0) +public class GameCardRootPartition : IDisposable +{ + private const int LogoPartitionSizeMax = 0x12000; + private static ReadOnlySpan UpdatePartitionName => "update"u8; + private static ReadOnlySpan NormalPartitionName => "normal"u8; + private static ReadOnlySpan SecurePartitionName => "secure"u8; + private static ReadOnlySpan LogoPartitionName => "logo"u8; + private static ReadOnlySpan LogoPartitionPath => "/logo"u8; + + private UniqueRef _partitionFsMeta; + private SharedRef _alignedRootStorage; + private GameCardHandle _gcHandle; + private long _metaDataSize; + private IGameCardStorageCreator _gameCardStorageCreator; + private byte[] _logoPartitionData; + private SharedRef _logoPartitionStorage; + private SdkMutexType _mutex; + + // LibHac addition so we can access fssrv::storage functions + private readonly FileSystemServer _fsServer; + + public GameCardRootPartition(GameCardHandle handle, ref SharedRef rootStorage, + IGameCardStorageCreator storageCreator, ref UniqueRef partitionFsMeta, + FileSystemServer fsServer) + { + _partitionFsMeta = new UniqueRef(ref partitionFsMeta); + _alignedRootStorage = SharedRef.CreateMove(ref rootStorage); + _gcHandle = handle; + _gameCardStorageCreator = storageCreator; + _logoPartitionStorage = new SharedRef(); + _mutex = new SdkMutexType(); + _metaDataSize = _partitionFsMeta.Get.GetMetaDataSize(); + + _fsServer = fsServer; + } + + public void Dispose() + { + _logoPartitionStorage.Destroy(); + _alignedRootStorage.Destroy(); + _partitionFsMeta.Destroy(); + + if (_logoPartitionData is not null) + { + ArrayPool.Shared.Return(_logoPartitionData); + _logoPartitionData = null; + } + } + + private static U8Span GetPartitionPath(GameCardPartition partitionType) + { + switch (partitionType) + { + case GameCardPartition.Update: return UpdatePartitionName; + case GameCardPartition.Normal: return NormalPartitionName; + case GameCardPartition.Secure: return SecurePartitionName; + case GameCardPartition.Logo: return LogoPartitionName; + default: + Abort.UnexpectedDefault(); + return default; + } + } + + public bool IsValid() + { + Assert.SdkNotNull(in _alignedRootStorage); + + return _fsServer.Storage.IsGameCardActivationValid(_gcHandle); + } + + public Result OpenPartition(ref SharedRef outStorage, out ReadOnlyRef outEntry, + GameCardHandle handle, GameCardPartition partitionType) + { + outEntry = default; + + switch (partitionType) + { + case GameCardPartition.Update: + case GameCardPartition.Normal: + case GameCardPartition.Secure: + case GameCardPartition.Logo: + break; + default: + return ResultFs.InvalidArgument.Log(); + } + + int entryIndex = _partitionFsMeta.Get.GetEntryIndex(GetPartitionPath(partitionType)); + if (entryIndex < 0) + return ResultFs.PartitionNotFound.Log(); + + ref readonly PartitionEntry entry = ref _partitionFsMeta.Get.GetEntry(entryIndex); + + switch (partitionType) + { + case GameCardPartition.Update: + case GameCardPartition.Normal: + { + // The root partition contains the entire non-secure section of the game card, so we just need to make + // a SubStorage of the appropriate range. + outStorage.Reset(new SubStorage(in _alignedRootStorage, _metaDataSize + entry.Offset, entry.Size)); + + if (!outStorage.HasValue) + return ResultFs.AllocationMemoryFailedInGameCardFileSystemCreatorA.Log(); + + return Result.Success; + } + case GameCardPartition.Secure: + { + Result res = EnsureLogoDataCached(); + if (res.IsFailure() && !ResultFs.PartitionNotFound.Includes(res)) + return res.Miss(); + + using var secureStorage = new SharedRef(); + res = _gameCardStorageCreator.CreateSecureReadOnly(handle, ref secureStorage.Ref); + if (res.IsFailure()) return res.Miss(); + + const int dataAlignment = 512; + + using var alignedStorage = new SharedRef( + new AlignmentMatchingStorageInBulkRead(in secureStorage, + dataAlignment)); + + if (!alignedStorage.HasValue) + return ResultFs.AllocationMemoryFailedInGameCardFileSystemCreatorB.Log(); + + outStorage.SetByMove(ref alignedStorage.Ref); + return Result.Success; + } + case GameCardPartition.Logo: + { + Result res = EnsureLogoDataCached(); + if (res.IsFailure() && !ResultFs.PartitionNotFound.Includes(res)) + return res.Miss(); + + outStorage.SetByCopy(in _logoPartitionStorage); + return Result.Success; + } + } + + return ResultFs.PartitionNotFound.Log(); + } + + public Result EnsureLogoDataCached() + { + int entryIndex = _partitionFsMeta.Get.GetEntryIndex(GetPartitionPath(GameCardPartition.Logo)); + if (entryIndex < 0) + return ResultFs.PartitionNotFound.Log(); + + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); + + if (_logoPartitionStorage.HasValue) + return Result.Success; + + ref readonly PartitionEntry logoEntry = ref _partitionFsMeta.Get.GetEntry(entryIndex); + if (logoEntry.Size > LogoPartitionSizeMax) + return ResultFs.GameCardLogoDataTooLarge.Log(); + + using var rootPartitionFs = new Sha256PartitionFileSystem(); + Result res = rootPartitionFs.Initialize(_partitionFsMeta.Get, in _alignedRootStorage); + if (res.IsFailure()) return res.Miss(); + + using var file = new UniqueRef(); + + using var pathLogo = new Path(); + res = PathFunctions.SetUpFixedPath(ref pathLogo.Ref(), LogoPartitionPath); + if (res.IsFailure()) return res.Miss(); + + res = rootPartitionFs.OpenFile(ref file.Ref, in pathLogo, OpenMode.Read); + if (res.IsFailure()) return res.Miss(); + + _logoPartitionData = ArrayPool.Shared.Rent(LogoPartitionSizeMax); + + res = file.Get.Read(out long readSize, offset: 0, _logoPartitionData.AsSpan(0, LogoPartitionSizeMax), + ReadOption.None); + if (ResultFs.DataCorrupted.Includes(res)) + return ResultFs.GameCardLogoDataCorrupted.LogConverted(res); + + if (res.IsFailure()) return res.Miss(); + + if (readSize != logoEntry.Size) + return ResultFs.GameCardLogoDataSizeInvalid.Log(); + + _logoPartitionStorage.Reset(new MemoryStorage(_logoPartitionData, (int)logoEntry.Size)); + + return Result.Success; + } +} + +/// +/// Creates s of the various partitions contained by the currently mounted game card. +/// +/// Based on nnSdk 15.3.0 (FS 15.0.0) +public class GameCardFileSystemCreator : IGameCardFileSystemCreator +{ + private MemoryResource _allocator; + private GameCardStorageCreator _gameCardStorageCreator; + private UniqueRef _rootPartition; + private SdkMutexType _mutex; + + // LibHac addition so we can access fssrv::storage functions + private readonly FileSystemServer _fsServer; + + public GameCardFileSystemCreator(MemoryResource allocator, GameCardStorageCreator gameCardStorageCreator, + FileSystemServer fsServer) + { + _allocator = allocator; + _gameCardStorageCreator = gameCardStorageCreator; + _rootPartition = new UniqueRef(); + _mutex = new SdkMutexType(); + + _fsServer = fsServer; + } + + public void Dispose() + { + _rootPartition.Destroy(); + } + + public Result Create(ref SharedRef outFileSystem, GameCardHandle handle, GameCardPartition partitionType) + { + Result res; + + using (ScopedLock.Lock(ref _mutex)) + { + // Initialize the root partition if not already initialized. + if (!_rootPartition.HasValue || !_rootPartition.Get.IsValid()) + { + // Open an IStorage of the game card's normal area. + using var rootStorage = new SharedRef(); + res = _gameCardStorageCreator.CreateReadOnly(handle, ref rootStorage.Ref); + if (res.IsFailure()) return res.Miss(); + + // Make sure reads to the game card are aligned to the game card's sector size. + const int dataAlignment = 512; + using var alignedRootStorage = new SharedRef( + new AlignmentMatchingStorageInBulkRead(in rootStorage, dataAlignment)); + + if (!alignedRootStorage.HasValue) + return ResultFs.AllocationMemoryFailedInGameCardFileSystemCreatorC.Log(); + + res = _fsServer.Storage.GetGameCardStatus(out GameCardStatus status, handle); + if (res.IsFailure()) return res.Miss(); + + // Get an IStorage of the start of the root partition to the start of the secure area. + long updateAndNormalPartitionSize = status.SecureAreaOffset - status.PartitionFsHeaderOffset; + using var rootFsStorage = new SharedRef(new SubStorage(in alignedRootStorage, + status.PartitionFsHeaderOffset, updateAndNormalPartitionSize)); + + if (!rootFsStorage.HasValue) + return ResultFs.AllocationMemoryFailedInGameCardFileSystemCreatorD.Log(); + + // Initialize a reader for the root partition. + using var rootPartitionFsMeta = new UniqueRef(new Sha256PartitionFileSystemMeta()); + if (!rootPartitionFsMeta.HasValue) + return ResultFs.AllocationMemoryFailedInGameCardFileSystemCreatorG.Log(); + + res = GetSaltFromCompatibilityType(out Optional salt, status.CompatibilityType); + if (res.IsFailure()) return res.Miss(); + + res = rootPartitionFsMeta.Get.Initialize(rootFsStorage.Get, _allocator, status.PartitionFsHeaderHash, salt); + if (res.IsFailure()) return res.Miss(); + + _rootPartition.Reset(new GameCardRootPartition(handle, ref rootFsStorage.Ref, _gameCardStorageCreator, + ref rootPartitionFsMeta.Ref, _fsServer)); + + if (!_rootPartition.HasValue) + return ResultFs.AllocationMemoryFailedInGameCardFileSystemCreatorE.Log(); + } + } + + // Open the raw storage of the requested partition. + using var partitionStorage = new SharedRef(); + res = _rootPartition.Get.OpenPartition(ref partitionStorage.Ref, out ReadOnlyRef refEntry, + handle, partitionType); + if (res.IsFailure()) return res.Miss(); + + // Initialize a Sha256PartitionFileSystem for reading the partition's file system. + using var partitionFsMeta = new UniqueRef(new Sha256PartitionFileSystemMeta()); + if (!partitionFsMeta.HasValue) + return ResultFs.AllocationMemoryFailedInGameCardFileSystemCreatorH.Log(); + + if (partitionType == GameCardPartition.Logo) + { + res = partitionFsMeta.Get.Initialize(partitionStorage.Get, _allocator); + if (res.IsFailure()) return res.Miss(); + } + else + { + res = partitionFsMeta.Get.Initialize(partitionStorage.Get, _allocator, refEntry.Value.Hash); + if (res.IsFailure()) return res.Miss(); + } + + res = Sha256PartitionFileSystemMeta.QueryMetaDataSize(out _, partitionStorage.Get); + if (res.IsFailure()) return res.Miss(); + + using var fs = new SharedRef(new Sha256PartitionFileSystem()); + if (!fs.HasValue) + return ResultFs.AllocationMemoryFailedInGameCardFileSystemCreatorF.Log(); + + res = fs.Get.Initialize(ref partitionFsMeta.Ref, in partitionStorage); + if (res.IsFailure()) return res.Miss(); + + outFileSystem.SetByMove(ref fs.Ref); + return Result.Success; + } + + private Result GetSaltFromCompatibilityType(out Optional outSalt, byte compatibilityType) + { + switch ((GameCardCompatibilityType)compatibilityType) + { + case GameCardCompatibilityType.Normal: + outSalt = default; + break; + case GameCardCompatibilityType.Terra: + outSalt = new Optional(compatibilityType); + break; + default: + outSalt = default; + return ResultFs.GameCardFsInvalidCompatibilityType.Log(); + } + + return Result.Success; + } +} \ No newline at end of file diff --git a/src/LibHac/FsSrv/FsCreator/GameCardStorageCreator.cs b/src/LibHac/FsSrv/FsCreator/GameCardStorageCreator.cs new file mode 100644 index 00000000..c40bed32 --- /dev/null +++ b/src/LibHac/FsSrv/FsCreator/GameCardStorageCreator.cs @@ -0,0 +1,38 @@ +using LibHac.Common; +using LibHac.Fs; +using LibHac.FsSrv.Storage; +using LibHac.GcSrv; + +namespace LibHac.FsSrv.FsCreator; + +/// +/// Creates s to the currently mounted game card. +/// +/// Based on nnSdk 15.3.0 (FS 15.0.0) +public class GameCardStorageCreator : IGameCardStorageCreator +{ + // LibHac addition so we can access fssrv::storage functions + private readonly FileSystemServer _fsServer; + + public GameCardStorageCreator(FileSystemServer fsServer) + { + _fsServer = fsServer; + } + + public void Dispose() { } + + public Result CreateReadOnly(GameCardHandle handle, ref SharedRef outStorage) + { + return _fsServer.Storage.OpenGameCardStorage(ref outStorage, OpenGameCardAttribute.ReadOnly, handle).Ret(); + } + + public Result CreateSecureReadOnly(GameCardHandle handle, ref SharedRef outStorage) + { + return _fsServer.Storage.OpenGameCardStorage(ref outStorage, OpenGameCardAttribute.SecureReadOnly, handle).Ret(); + } + + public Result CreateWriteOnly(GameCardHandle handle, ref SharedRef outStorage) + { + return _fsServer.Storage.OpenGameCardStorage(ref outStorage, OpenGameCardAttribute.WriteOnly, handle).Ret(); + } +} \ No newline at end of file diff --git a/src/LibHac/FsSrv/FsCreator/IGameCardFileSystemCreator.cs b/src/LibHac/FsSrv/FsCreator/IGameCardFileSystemCreator.cs index c6ee3577..e40a9a4d 100644 --- a/src/LibHac/FsSrv/FsCreator/IGameCardFileSystemCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/IGameCardFileSystemCreator.cs @@ -1,10 +1,11 @@ -using LibHac.Common; +using System; +using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; namespace LibHac.FsSrv.FsCreator; -public interface IGameCardFileSystemCreator +public interface IGameCardFileSystemCreator : IDisposable { Result Create(ref SharedRef outFileSystem, GameCardHandle handle, GameCardPartition partitionType); } \ No newline at end of file diff --git a/src/LibHac/FsSrv/FsCreator/IGameCardStorageCreator.cs b/src/LibHac/FsSrv/FsCreator/IGameCardStorageCreator.cs index a14159ed..95b47485 100644 --- a/src/LibHac/FsSrv/FsCreator/IGameCardStorageCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/IGameCardStorageCreator.cs @@ -1,9 +1,10 @@ -using LibHac.Common; +using System; +using LibHac.Common; using LibHac.Fs; namespace LibHac.FsSrv.FsCreator; -public interface IGameCardStorageCreator +public interface IGameCardStorageCreator : IDisposable { Result CreateReadOnly(GameCardHandle handle, ref SharedRef outStorage); Result CreateSecureReadOnly(GameCardHandle handle, ref SharedRef outStorage); diff --git a/src/LibHac/FsSystem/AlignmentMatchingStorage.cs b/src/LibHac/FsSystem/AlignmentMatchingStorage.cs index fdb62fb5..40bba64f 100644 --- a/src/LibHac/FsSystem/AlignmentMatchingStorage.cs +++ b/src/LibHac/FsSystem/AlignmentMatchingStorage.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using LibHac.Common; using LibHac.Diag; using LibHac.Fs; @@ -8,11 +7,25 @@ using LibHac.Util; namespace LibHac.FsSystem; -public interface IAlignmentMatchingStorageSize { } +public interface IAlignmentMatchingStorageSize +{ + static abstract uint Alignment { get; } +} -[StructLayout(LayoutKind.Sequential, Size = 1)] public struct AlignmentMatchingStorageSize1 : IAlignmentMatchingStorageSize { } -[StructLayout(LayoutKind.Sequential, Size = 16)] public struct AlignmentMatchingStorageSize16 : IAlignmentMatchingStorageSize { } -[StructLayout(LayoutKind.Sequential, Size = 512)] public struct AlignmentMatchingStorageSize512 : IAlignmentMatchingStorageSize { } +public struct AlignmentMatchingStorageSize1 : IAlignmentMatchingStorageSize +{ + public static uint Alignment => 1; +} + +public struct AlignmentMatchingStorageSize16 : IAlignmentMatchingStorageSize +{ + public static uint Alignment => 16; +} + +public struct AlignmentMatchingStorageSize512 : IAlignmentMatchingStorageSize +{ + public static uint Alignment => 512; +} /// /// Handles accessing a base that must always be accessed via an aligned offset and size. @@ -29,8 +42,8 @@ public class AlignmentMatchingStorage : IStora where TDataAlignment : struct, IAlignmentMatchingStorageSize where TBufferAlignment : struct, IAlignmentMatchingStorageSize { - public static uint DataAlign => (uint)Unsafe.SizeOf(); - public static uint BufferAlign => (uint)Unsafe.SizeOf(); + public static uint DataAlign => TDataAlignment.Alignment; + public static uint BufferAlign => TBufferAlignment.Alignment; public static uint DataAlignMax => 0x200; @@ -170,7 +183,7 @@ public class AlignmentMatchingStorage : IStora public class AlignmentMatchingStoragePooledBuffer : IStorage where TBufferAlignment : struct, IAlignmentMatchingStorageSize { - public static uint BufferAlign => (uint)Unsafe.SizeOf(); + public static uint BufferAlign => TBufferAlignment.Alignment; private IStorage _baseStorage; private long _baseStorageSize; @@ -314,7 +327,7 @@ public class AlignmentMatchingStoragePooledBuffer : IStorage public class AlignmentMatchingStorageInBulkRead : IStorage where TBufferAlignment : struct, IAlignmentMatchingStorageSize { - public static uint BufferAlign => (uint)Unsafe.SizeOf(); + public static uint BufferAlign => TBufferAlignment.Alignment; private IStorage _baseStorage; private SharedRef _sharedBaseStorage;