2021-12-03 00:15:44 +01:00
|
|
|
|
using System;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
using LibHac.Fs;
|
|
|
|
|
using LibHac.FsSystem;
|
|
|
|
|
using LibHac.Tests.Common;
|
|
|
|
|
using LibHac.Tests.Fs;
|
|
|
|
|
using Xunit;
|
|
|
|
|
|
|
|
|
|
namespace LibHac.Tests.FsSystem;
|
|
|
|
|
|
|
|
|
|
public class IndirectStorageBuffers
|
|
|
|
|
{
|
|
|
|
|
public IndirectStorageTests.IndirectStorageData[] Buffers { get; }
|
|
|
|
|
|
|
|
|
|
public IndirectStorageBuffers()
|
|
|
|
|
{
|
|
|
|
|
IndirectStorageTests.IndirectStorageTestConfig[] storageConfig = IndirectStorageTests.IndirectStorageTestData;
|
|
|
|
|
Buffers = new IndirectStorageTests.IndirectStorageData[storageConfig.Length];
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < storageConfig.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
IndirectStorageTests.IndirectStorageTestConfig config = storageConfig[i];
|
|
|
|
|
|
|
|
|
|
SizeRange patchSizeRange = config.PatchEntrySizeRange.BlockSize == 0
|
|
|
|
|
? config.OriginalEntrySizeRange
|
|
|
|
|
: config.PatchEntrySizeRange;
|
|
|
|
|
|
|
|
|
|
Buffers[i] = IndirectStorageCreator.Create(config.RngSeed, config.OriginalEntrySizeRange, patchSizeRange,
|
|
|
|
|
config.StorageSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class IndirectStorageTests : IClassFixture<IndirectStorageBuffers>
|
|
|
|
|
{
|
|
|
|
|
// Keep the generated data between tests so it only has to be generated once
|
|
|
|
|
private readonly IndirectStorageData[] _storageBuffers;
|
|
|
|
|
|
|
|
|
|
public IndirectStorageTests(IndirectStorageBuffers buffers)
|
|
|
|
|
{
|
|
|
|
|
_storageBuffers = buffers.Buffers;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class IndirectStorageTestConfig
|
|
|
|
|
{
|
|
|
|
|
public ulong RngSeed { get; init; }
|
|
|
|
|
public long StorageSize { get; init; }
|
|
|
|
|
|
|
|
|
|
// If the patch size range is left blank, the same values will be used for both the original and patch entry sizes
|
|
|
|
|
public SizeRange OriginalEntrySizeRange { get; init; }
|
|
|
|
|
public SizeRange PatchEntrySizeRange { get; init; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class RandomAccessTestConfig
|
|
|
|
|
{
|
|
|
|
|
public int[] SizeClassProbs { get; init; }
|
|
|
|
|
public int[] SizeClassMaxSizes { get; init; }
|
|
|
|
|
public int[] TaskProbs { get; init; }
|
|
|
|
|
public int[] AccessTypeProbs { get; init; }
|
|
|
|
|
public ulong RngSeed { get; init; }
|
|
|
|
|
public int FrequentAccessBlockCount { get; init; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static readonly IndirectStorageTestConfig[] IndirectStorageTestData =
|
2023-12-10 02:48:56 +01:00
|
|
|
|
[
|
2021-12-03 00:15:44 +01:00
|
|
|
|
// Small patched regions to force continuous reading
|
|
|
|
|
new()
|
|
|
|
|
{
|
|
|
|
|
RngSeed = 948285,
|
2022-11-12 02:21:07 +01:00
|
|
|
|
OriginalEntrySizeRange = new SizeRange(0x10000, 1, 5),
|
2021-12-03 00:15:44 +01:00
|
|
|
|
PatchEntrySizeRange = new SizeRange(1, 0x20, 0xFFF),
|
|
|
|
|
StorageSize = 1024 * 1024 * 10
|
|
|
|
|
},
|
|
|
|
|
// Small patch regions
|
|
|
|
|
new()
|
|
|
|
|
{
|
|
|
|
|
RngSeed = 236956,
|
2022-11-12 02:21:07 +01:00
|
|
|
|
OriginalEntrySizeRange = new SizeRange(0x1000, 1, 10),
|
2021-12-03 00:15:44 +01:00
|
|
|
|
StorageSize = 1024 * 1024 * 10
|
|
|
|
|
},
|
|
|
|
|
// Medium patch regions
|
|
|
|
|
new()
|
|
|
|
|
{
|
|
|
|
|
RngSeed = 352174,
|
2022-11-12 02:21:07 +01:00
|
|
|
|
OriginalEntrySizeRange = new SizeRange(0x8000, 1, 10),
|
2021-12-03 00:15:44 +01:00
|
|
|
|
StorageSize = 1024 * 1024 * 10
|
|
|
|
|
},
|
|
|
|
|
// Larger patch regions
|
|
|
|
|
new()
|
|
|
|
|
{
|
|
|
|
|
RngSeed = 220754,
|
2022-11-12 02:21:07 +01:00
|
|
|
|
OriginalEntrySizeRange = new SizeRange(0x10000, 10, 50),
|
2021-12-03 00:15:44 +01:00
|
|
|
|
StorageSize = 1024 * 1024 * 10
|
|
|
|
|
}
|
2023-12-10 02:48:56 +01:00
|
|
|
|
];
|
2021-12-03 00:15:44 +01:00
|
|
|
|
|
|
|
|
|
private static readonly RandomAccessTestConfig[] AccessTestConfigs =
|
2023-12-10 02:48:56 +01:00
|
|
|
|
[
|
2021-12-03 00:15:44 +01:00
|
|
|
|
new()
|
|
|
|
|
{
|
2023-12-10 02:48:56 +01:00
|
|
|
|
SizeClassProbs = [50, 50, 5],
|
|
|
|
|
SizeClassMaxSizes = [0x4000, 0x80000, 0x800000], // 16 KB, 512 KB, 8 MB
|
|
|
|
|
TaskProbs = [1, 0, 0], // Read, Write, Flush
|
|
|
|
|
AccessTypeProbs = [10, 10, 5], // Random, Sequential, Frequent block
|
2021-12-03 00:15:44 +01:00
|
|
|
|
RngSeed = 35467,
|
|
|
|
|
FrequentAccessBlockCount = 6,
|
|
|
|
|
},
|
|
|
|
|
new()
|
|
|
|
|
{
|
2023-12-10 02:48:56 +01:00
|
|
|
|
SizeClassProbs = [50, 50, 5],
|
|
|
|
|
SizeClassMaxSizes = [0x800, 0x1000, 0x8000], // 2 KB, 4 KB, 32 KB
|
|
|
|
|
TaskProbs = [1, 0, 0], // Read, Write, Flush
|
|
|
|
|
AccessTypeProbs = [1, 10, 0], // Random, Sequential, Frequent block
|
2021-12-03 00:15:44 +01:00
|
|
|
|
RngSeed = 13579
|
|
|
|
|
},
|
2023-12-10 02:48:56 +01:00
|
|
|
|
];
|
2021-12-03 00:15:44 +01:00
|
|
|
|
|
|
|
|
|
public static TheoryData<int> IndirectStorageTestTheoryData =
|
|
|
|
|
TheoryDataCreator.CreateSequence(0, IndirectStorageTestData.Length);
|
|
|
|
|
|
|
|
|
|
public class IndirectStorageData
|
|
|
|
|
{
|
|
|
|
|
public IndirectStorage.Entry[] TableEntries;
|
|
|
|
|
public byte[] TableHeaderBuffer;
|
|
|
|
|
public byte[] TableNodeBuffer;
|
|
|
|
|
public byte[] TableEntryBuffer;
|
|
|
|
|
|
|
|
|
|
public byte[] SparseTableHeaderBuffer;
|
|
|
|
|
public byte[] SparseTableNodeBuffer;
|
|
|
|
|
public byte[] SparseTableEntryBuffer;
|
|
|
|
|
|
|
|
|
|
public byte[] OriginalStorageBuffer;
|
|
|
|
|
public byte[] SparseOriginalStorageBuffer;
|
|
|
|
|
public byte[] PatchStorageBuffer;
|
|
|
|
|
public byte[] PatchedStorageBuffer;
|
|
|
|
|
|
|
|
|
|
public IndirectStorage CreateIndirectStorage(bool useSparseOriginalStorage)
|
|
|
|
|
{
|
|
|
|
|
BucketTree.Header header = MemoryMarshal.Cast<byte, BucketTree.Header>(TableHeaderBuffer)[0];
|
|
|
|
|
|
|
|
|
|
using var nodeStorage = new ValueSubStorage(new MemoryStorage(TableNodeBuffer), 0, TableNodeBuffer.Length);
|
|
|
|
|
using var entryStorage = new ValueSubStorage(new MemoryStorage(TableEntryBuffer), 0, TableEntryBuffer.Length);
|
|
|
|
|
|
|
|
|
|
IStorage originalStorageBase = useSparseOriginalStorage ? CreateSparseStorage() : new MemoryStorage(OriginalStorageBuffer);
|
|
|
|
|
|
|
|
|
|
using var originalStorage = new ValueSubStorage(originalStorageBase, 0, OriginalStorageBuffer.Length);
|
|
|
|
|
using var patchStorage = new ValueSubStorage(new MemoryStorage(PatchStorageBuffer), 0, PatchStorageBuffer.Length);
|
|
|
|
|
|
|
|
|
|
var storage = new IndirectStorage();
|
|
|
|
|
Assert.Success(storage.Initialize(new ArrayPoolMemoryResource(), in nodeStorage, in entryStorage, header.EntryCount));
|
|
|
|
|
storage.SetStorage(0, in originalStorage);
|
|
|
|
|
storage.SetStorage(1, in patchStorage);
|
|
|
|
|
|
|
|
|
|
return storage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SparseStorage CreateSparseStorage()
|
|
|
|
|
{
|
|
|
|
|
BucketTree.Header header = MemoryMarshal.Cast<byte, BucketTree.Header>(SparseTableHeaderBuffer)[0];
|
|
|
|
|
|
|
|
|
|
using var nodeStorage = new ValueSubStorage(new MemoryStorage(SparseTableNodeBuffer), 0, SparseTableNodeBuffer.Length);
|
|
|
|
|
using var entryStorage = new ValueSubStorage(new MemoryStorage(SparseTableEntryBuffer), 0, SparseTableEntryBuffer.Length);
|
|
|
|
|
|
|
|
|
|
using var sparseOriginalStorage = new ValueSubStorage(new MemoryStorage(SparseOriginalStorageBuffer), 0, SparseOriginalStorageBuffer.Length);
|
|
|
|
|
|
|
|
|
|
var sparseStorage = new SparseStorage();
|
|
|
|
|
Assert.Success(sparseStorage.Initialize(new ArrayPoolMemoryResource(), in nodeStorage, in entryStorage, header.EntryCount));
|
|
|
|
|
sparseStorage.SetDataStorage(in sparseOriginalStorage);
|
|
|
|
|
|
|
|
|
|
return sparseStorage;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory, MemberData(nameof(IndirectStorageTestTheoryData))]
|
|
|
|
|
public void Read_EntireStorageInSingleRead_DataIsCorrect(int index)
|
|
|
|
|
{
|
|
|
|
|
ReadEntireStorageImpl(index, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory, MemberData(nameof(IndirectStorageTestTheoryData))]
|
|
|
|
|
public void Read_EntireStorageInSingleRead_OriginalStorageIsSparse_DataIsCorrect(int index)
|
|
|
|
|
{
|
|
|
|
|
ReadEntireStorageImpl(index, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ReadEntireStorageImpl(int index, bool useSparseOriginalStorage)
|
|
|
|
|
{
|
|
|
|
|
using IndirectStorage storage = _storageBuffers[index].CreateIndirectStorage(useSparseOriginalStorage);
|
|
|
|
|
|
|
|
|
|
byte[] expectedPatchedData = _storageBuffers[index].PatchedStorageBuffer;
|
|
|
|
|
byte[] actualPatchedData = new byte[expectedPatchedData.Length];
|
|
|
|
|
|
|
|
|
|
Assert.Success(storage.GetSize(out long storageSize));
|
|
|
|
|
Assert.Equal(actualPatchedData.Length, storageSize);
|
|
|
|
|
|
|
|
|
|
Assert.Success(storage.Read(0, actualPatchedData));
|
|
|
|
|
Assert.True(expectedPatchedData.SequenceEqual(actualPatchedData));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Initialize_SingleTableStorage()
|
|
|
|
|
{
|
|
|
|
|
const int index = 1;
|
|
|
|
|
IndirectStorageData buffers = _storageBuffers[index];
|
|
|
|
|
|
|
|
|
|
byte[] tableBuffer = buffers.TableHeaderBuffer.Concat(buffers.TableNodeBuffer.Concat(buffers.TableEntryBuffer)).ToArray();
|
|
|
|
|
using var tableStorage = new ValueSubStorage(new MemoryStorage(tableBuffer), 0, tableBuffer.Length);
|
|
|
|
|
|
|
|
|
|
using var originalStorage = new ValueSubStorage(new MemoryStorage(buffers.OriginalStorageBuffer), 0, buffers.OriginalStorageBuffer.Length);
|
|
|
|
|
using var patchStorage = new ValueSubStorage(new MemoryStorage(buffers.PatchStorageBuffer), 0, buffers.PatchStorageBuffer.Length);
|
|
|
|
|
|
|
|
|
|
using var storage = new IndirectStorage();
|
|
|
|
|
Assert.Success(storage.Initialize(new ArrayPoolMemoryResource(), in tableStorage));
|
|
|
|
|
storage.SetStorage(0, in originalStorage);
|
|
|
|
|
storage.SetStorage(1, in patchStorage);
|
|
|
|
|
|
|
|
|
|
byte[] expectedPatchedData = _storageBuffers[index].PatchedStorageBuffer;
|
|
|
|
|
byte[] actualPatchedData = new byte[expectedPatchedData.Length];
|
|
|
|
|
|
|
|
|
|
Assert.Success(storage.GetSize(out long storageSize));
|
|
|
|
|
Assert.Equal(actualPatchedData.Length, storageSize);
|
|
|
|
|
|
|
|
|
|
Assert.Success(storage.Read(0, actualPatchedData));
|
|
|
|
|
Assert.True(expectedPatchedData.SequenceEqual(actualPatchedData));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory, MemberData(nameof(IndirectStorageTestTheoryData))]
|
|
|
|
|
public void Read_RandomAccess_DataIsCorrect(int index)
|
|
|
|
|
{
|
|
|
|
|
foreach (RandomAccessTestConfig accessConfig in AccessTestConfigs)
|
|
|
|
|
{
|
|
|
|
|
StorageTester tester = SetupRandomAccessTest(index, accessConfig, true);
|
|
|
|
|
tester.Run(0x1000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory, MemberData(nameof(IndirectStorageTestTheoryData))]
|
|
|
|
|
public void GetEntryList_GetAllEntries_ReturnsCorrectEntries(int index)
|
|
|
|
|
{
|
|
|
|
|
GetEntryListTestImpl(index, 0, _storageBuffers[index].PatchedStorageBuffer.Length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory, MemberData(nameof(IndirectStorageTestTheoryData))]
|
|
|
|
|
public void GetEntryList_GetPartialEntries_ReturnsCorrectEntries(int index)
|
|
|
|
|
{
|
|
|
|
|
IndirectStorageData buffers = _storageBuffers[index];
|
|
|
|
|
var random = new Random(IndirectStorageTestData[index].RngSeed);
|
|
|
|
|
|
|
|
|
|
int endOffset = buffers.PatchedStorageBuffer.Length;
|
|
|
|
|
int maxSize = endOffset / 2;
|
|
|
|
|
const int testCount = 100;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < testCount; i++)
|
|
|
|
|
{
|
|
|
|
|
long offset = random.Next(0, endOffset);
|
|
|
|
|
long size = Math.Min(endOffset - offset, random.Next(0, maxSize));
|
|
|
|
|
|
|
|
|
|
GetEntryListTestImpl(index, offset, size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GetEntryListTestImpl(index, 0, _storageBuffers[index].PatchedStorageBuffer.Length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void GetEntryListTestImpl(int index, long offset, long size)
|
|
|
|
|
{
|
|
|
|
|
Assert.True(size > 0);
|
|
|
|
|
|
|
|
|
|
IndirectStorageData buffers = _storageBuffers[index];
|
|
|
|
|
using IndirectStorage storage = buffers.CreateIndirectStorage(false);
|
|
|
|
|
IndirectStorage.Entry[] entries = buffers.TableEntries;
|
|
|
|
|
int endOffset = buffers.PatchedStorageBuffer.Length;
|
|
|
|
|
|
|
|
|
|
int startIndex = FindEntry(entries, offset, endOffset);
|
|
|
|
|
int endIndex = FindEntry(entries, offset + size - 1, endOffset);
|
|
|
|
|
int count = endIndex - startIndex + 1;
|
|
|
|
|
|
|
|
|
|
Span<IndirectStorage.Entry> expectedEntries = buffers.TableEntries.AsSpan(startIndex, count);
|
|
|
|
|
var actualEntries = new IndirectStorage.Entry[expectedEntries.Length + 1];
|
|
|
|
|
|
|
|
|
|
Assert.Success(storage.GetEntryList(actualEntries, out int entryCount, offset, size));
|
|
|
|
|
|
|
|
|
|
Assert.Equal(expectedEntries.Length, entryCount);
|
|
|
|
|
Assert.True(actualEntries.AsSpan(0, entryCount).SequenceEqual(expectedEntries));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int FindEntry(IndirectStorage.Entry[] entries, long offset, long endOffset)
|
|
|
|
|
{
|
|
|
|
|
Assert.True(offset >= 0);
|
|
|
|
|
Assert.True(offset < endOffset);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i + 1 < entries.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
if (offset >= entries[i].GetVirtualOffset() && offset < entries[i + 1].GetVirtualOffset())
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return entries.Length - 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory, MemberData(nameof(IndirectStorageTestTheoryData))]
|
|
|
|
|
public void SparseStorage_Read_EntireStorageInSingleRead_DataIsCorrect(int index)
|
|
|
|
|
{
|
|
|
|
|
IndirectStorageData buffers = _storageBuffers[index];
|
|
|
|
|
using SparseStorage storage = buffers.CreateSparseStorage();
|
|
|
|
|
|
|
|
|
|
byte[] expectedPatchedData = buffers.OriginalStorageBuffer;
|
|
|
|
|
byte[] actualPatchedData = new byte[expectedPatchedData.Length];
|
|
|
|
|
|
|
|
|
|
Assert.Success(storage.GetSize(out long storageSize));
|
|
|
|
|
Assert.Equal(actualPatchedData.Length, storageSize);
|
|
|
|
|
|
|
|
|
|
Assert.Success(storage.Read(0, actualPatchedData));
|
|
|
|
|
Assert.True(expectedPatchedData.SequenceEqual(actualPatchedData));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory, MemberData(nameof(IndirectStorageTestTheoryData))]
|
|
|
|
|
public void SparseStorage_Read_RandomAccess_DataIsCorrect(int index)
|
|
|
|
|
{
|
|
|
|
|
foreach (RandomAccessTestConfig accessConfig in AccessTestConfigs)
|
|
|
|
|
{
|
|
|
|
|
StorageTester tester = SetupRandomAccessTest(index, accessConfig, true);
|
|
|
|
|
tester.Run(0x1000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private StorageTester SetupRandomAccessTest(int storageConfigIndex, RandomAccessTestConfig accessConfig, bool getSparseStorage)
|
|
|
|
|
{
|
|
|
|
|
IStorage indirectStorage = getSparseStorage
|
|
|
|
|
? _storageBuffers[storageConfigIndex].CreateSparseStorage()
|
|
|
|
|
: _storageBuffers[storageConfigIndex].CreateIndirectStorage(false);
|
|
|
|
|
|
|
|
|
|
Assert.Success(indirectStorage.GetSize(out long storageSize));
|
|
|
|
|
|
|
|
|
|
byte[] expectedStorageArray = new byte[storageSize];
|
|
|
|
|
Assert.Success(indirectStorage.Read(0, expectedStorageArray));
|
|
|
|
|
|
|
|
|
|
var memoryStorage = new MemoryStorage(expectedStorageArray);
|
|
|
|
|
|
|
|
|
|
var memoryStorageEntry = new StorageTester.Entry(memoryStorage, expectedStorageArray);
|
|
|
|
|
var indirectStorageEntry = new StorageTester.Entry(indirectStorage, expectedStorageArray);
|
|
|
|
|
|
|
|
|
|
var testerConfig = new StorageTester.Configuration()
|
|
|
|
|
{
|
2023-12-10 02:48:56 +01:00
|
|
|
|
Entries = [memoryStorageEntry, indirectStorageEntry],
|
2021-12-03 00:15:44 +01:00
|
|
|
|
SizeClassProbs = accessConfig.SizeClassProbs,
|
|
|
|
|
SizeClassMaxSizes = accessConfig.SizeClassMaxSizes,
|
|
|
|
|
TaskProbs = accessConfig.TaskProbs,
|
|
|
|
|
AccessTypeProbs = accessConfig.AccessTypeProbs,
|
|
|
|
|
RngSeed = accessConfig.RngSeed,
|
|
|
|
|
FrequentAccessBlockCount = accessConfig.FrequentAccessBlockCount
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return new StorageTester(testerConfig);
|
|
|
|
|
}
|
|
|
|
|
}
|