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;