Add GameCardStorageCreator and GameCardFileSystemCreator

This commit is contained in:
Alex Barney 2023-01-03 16:12:27 -07:00
parent a55b1d7c58
commit 80588438c0
8 changed files with 435 additions and 20 deletions

View file

@ -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;
/// <remarks>Based on nnSdk 14.3.0 (FS 14.1.0)</remarks>
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<byte> 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;
}

View file

@ -17,6 +17,8 @@ public class EmulatedGameCardFsCreator : IGameCardFileSystemCreator
_gameCard = gameCard;
}
public void Dispose() { }
public Result Create(ref SharedRef<IFileSystem> outFileSystem, GameCardHandle handle,
GameCardPartition partitionType)
{

View file

@ -13,6 +13,8 @@ public class EmulatedGameCardStorageCreator : IGameCardStorageCreator
GameCard = gameCard;
}
public void Dispose() { }
public Result CreateReadOnly(GameCardHandle handle, ref SharedRef<IStorage> outStorage)
{
if (GameCard.IsGameCardHandleInvalid(handle))

View file

@ -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;
/// <summary>
/// Reads the root partition of a game card and handles opening the various partitions it contains.
/// </summary>
/// <remarks>Based on nnSdk 15.3.0 (FS 15.0.0)</remarks>
public class GameCardRootPartition : IDisposable
{
private const int LogoPartitionSizeMax = 0x12000;
private static ReadOnlySpan<byte> UpdatePartitionName => "update"u8;
private static ReadOnlySpan<byte> NormalPartitionName => "normal"u8;
private static ReadOnlySpan<byte> SecurePartitionName => "secure"u8;
private static ReadOnlySpan<byte> LogoPartitionName => "logo"u8;
private static ReadOnlySpan<byte> LogoPartitionPath => "/logo"u8;
private UniqueRef<Sha256PartitionFileSystemMeta> _partitionFsMeta;
private SharedRef<IStorage> _alignedRootStorage;
private GameCardHandle _gcHandle;
private long _metaDataSize;
private IGameCardStorageCreator _gameCardStorageCreator;
private byte[] _logoPartitionData;
private SharedRef<IStorage> _logoPartitionStorage;
private SdkMutexType _mutex;
// LibHac addition so we can access fssrv::storage functions
private readonly FileSystemServer _fsServer;
public GameCardRootPartition(GameCardHandle handle, ref SharedRef<IStorage> rootStorage,
IGameCardStorageCreator storageCreator, ref UniqueRef<Sha256PartitionFileSystemMeta> partitionFsMeta,
FileSystemServer fsServer)
{
_partitionFsMeta = new UniqueRef<Sha256PartitionFileSystemMeta>(ref partitionFsMeta);
_alignedRootStorage = SharedRef<IStorage>.CreateMove(ref rootStorage);
_gcHandle = handle;
_gameCardStorageCreator = storageCreator;
_logoPartitionStorage = new SharedRef<IStorage>();
_mutex = new SdkMutexType();
_metaDataSize = _partitionFsMeta.Get.GetMetaDataSize();
_fsServer = fsServer;
}
public void Dispose()
{
_logoPartitionStorage.Destroy();
_alignedRootStorage.Destroy();
_partitionFsMeta.Destroy();
if (_logoPartitionData is not null)
{
ArrayPool<byte>.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<IStorage> outStorage, out ReadOnlyRef<PartitionEntry> 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<IStorage>();
res = _gameCardStorageCreator.CreateSecureReadOnly(handle, ref secureStorage.Ref);
if (res.IsFailure()) return res.Miss();
const int dataAlignment = 512;
using var alignedStorage = new SharedRef<IStorage>(
new AlignmentMatchingStorageInBulkRead<AlignmentMatchingStorageSize1>(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<SdkMutexType> 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<IFile>();
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<byte>.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;
}
}
/// <summary>
/// Creates <see cref="IFileSystem"/>s of the various partitions contained by the currently mounted game card.
/// </summary>
/// <remarks>Based on nnSdk 15.3.0 (FS 15.0.0)</remarks>
public class GameCardFileSystemCreator : IGameCardFileSystemCreator
{
private MemoryResource _allocator;
private GameCardStorageCreator _gameCardStorageCreator;
private UniqueRef<GameCardRootPartition> _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<GameCardRootPartition>();
_mutex = new SdkMutexType();
_fsServer = fsServer;
}
public void Dispose()
{
_rootPartition.Destroy();
}
public Result Create(ref SharedRef<IFileSystem> 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<IStorage>();
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<IStorage>(
new AlignmentMatchingStorageInBulkRead<AlignmentMatchingStorageSize1>(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<IStorage>(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<Sha256PartitionFileSystemMeta>(new Sha256PartitionFileSystemMeta());
if (!rootPartitionFsMeta.HasValue)
return ResultFs.AllocationMemoryFailedInGameCardFileSystemCreatorG.Log();
res = GetSaltFromCompatibilityType(out Optional<byte> 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<IStorage>();
res = _rootPartition.Get.OpenPartition(ref partitionStorage.Ref, out ReadOnlyRef<PartitionEntry> refEntry,
handle, partitionType);
if (res.IsFailure()) return res.Miss();
// Initialize a Sha256PartitionFileSystem for reading the partition's file system.
using var partitionFsMeta = new UniqueRef<Sha256PartitionFileSystemMeta>(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<Sha256PartitionFileSystem>(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<byte> outSalt, byte compatibilityType)
{
switch ((GameCardCompatibilityType)compatibilityType)
{
case GameCardCompatibilityType.Normal:
outSalt = default;
break;
case GameCardCompatibilityType.Terra:
outSalt = new Optional<byte>(compatibilityType);
break;
default:
outSalt = default;
return ResultFs.GameCardFsInvalidCompatibilityType.Log();
}
return Result.Success;
}
}

View file

@ -0,0 +1,38 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.FsSrv.Storage;
using LibHac.GcSrv;
namespace LibHac.FsSrv.FsCreator;
/// <summary>
/// Creates <see cref="IStorage"/>s to the currently mounted game card.
/// </summary>
/// <remarks>Based on nnSdk 15.3.0 (FS 15.0.0)</remarks>
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<IStorage> outStorage)
{
return _fsServer.Storage.OpenGameCardStorage(ref outStorage, OpenGameCardAttribute.ReadOnly, handle).Ret();
}
public Result CreateSecureReadOnly(GameCardHandle handle, ref SharedRef<IStorage> outStorage)
{
return _fsServer.Storage.OpenGameCardStorage(ref outStorage, OpenGameCardAttribute.SecureReadOnly, handle).Ret();
}
public Result CreateWriteOnly(GameCardHandle handle, ref SharedRef<IStorage> outStorage)
{
return _fsServer.Storage.OpenGameCardStorage(ref outStorage, OpenGameCardAttribute.WriteOnly, handle).Ret();
}
}

View file

@ -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<IFileSystem> outFileSystem, GameCardHandle handle, GameCardPartition partitionType);
}

View file

@ -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<IStorage> outStorage);
Result CreateSecureReadOnly(GameCardHandle handle, ref SharedRef<IStorage> outStorage);

View file

@ -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;
}
/// <summary>
/// Handles accessing a base <see cref="IStorage"/> that must always be accessed via an aligned offset and size.
@ -29,8 +42,8 @@ public class AlignmentMatchingStorage<TDataAlignment, TBufferAlignment> : IStora
where TDataAlignment : struct, IAlignmentMatchingStorageSize
where TBufferAlignment : struct, IAlignmentMatchingStorageSize
{
public static uint DataAlign => (uint)Unsafe.SizeOf<TDataAlignment>();
public static uint BufferAlign => (uint)Unsafe.SizeOf<TBufferAlignment>();
public static uint DataAlign => TDataAlignment.Alignment;
public static uint BufferAlign => TBufferAlignment.Alignment;
public static uint DataAlignMax => 0x200;
@ -170,7 +183,7 @@ public class AlignmentMatchingStorage<TDataAlignment, TBufferAlignment> : IStora
public class AlignmentMatchingStoragePooledBuffer<TBufferAlignment> : IStorage
where TBufferAlignment : struct, IAlignmentMatchingStorageSize
{
public static uint BufferAlign => (uint)Unsafe.SizeOf<TBufferAlignment>();
public static uint BufferAlign => TBufferAlignment.Alignment;
private IStorage _baseStorage;
private long _baseStorageSize;
@ -314,7 +327,7 @@ public class AlignmentMatchingStoragePooledBuffer<TBufferAlignment> : IStorage
public class AlignmentMatchingStorageInBulkRead<TBufferAlignment> : IStorage
where TBufferAlignment : struct, IAlignmentMatchingStorageSize
{
public static uint BufferAlign => (uint)Unsafe.SizeOf<TBufferAlignment>();
public static uint BufferAlign => TBufferAlignment.Alignment;
private IStorage _baseStorage;
private SharedRef<IStorage> _sharedBaseStorage;