mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2025-02-09 13:14:46 +01:00
Add FlatMapKeyValueStore with tests
This commit is contained in:
parent
672a0016b3
commit
37251968c0
@ -312,7 +312,7 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary
|
||||
10,811,819,RequestDeferred,
|
||||
10,812,,RequestDeferredByUser,
|
||||
|
||||
20,1,,TooLargeKeyOrDbFull,
|
||||
20,1,,OutOfKeyResource,There is no more space in the database or the key is too long.
|
||||
20,2,,KeyNotFound,
|
||||
20,4,,AllocationFailed,
|
||||
20,5,,InvalidKeyValue,
|
||||
|
Can't render this file because it has a wrong number of fields in line 153.
|
@ -21,9 +21,18 @@ namespace LibHac.Diag
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public static void NotNull<T>([NotNull] T item) where T : class
|
||||
public static void Null<T>([NotNull] T item) where T : class
|
||||
{
|
||||
if (!(item is null))
|
||||
{
|
||||
throw new LibHacException("Null assertion failed.");
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public static void NotNull<T>([NotNull] T item) where T : class
|
||||
{
|
||||
if (item is null)
|
||||
{
|
||||
throw new LibHacException("Not-null assertion failed.");
|
||||
}
|
||||
|
@ -316,7 +316,7 @@ namespace LibHac.FsService
|
||||
{
|
||||
if (reader.Indexer.IsFull())
|
||||
{
|
||||
return ResultKvdb.TooLargeKeyOrDbFull.Log();
|
||||
return ResultKvdb.OutOfKeyResource.Log();
|
||||
}
|
||||
}
|
||||
|
||||
|
35
src/LibHac/Kvdb/AutoBuffer.cs
Normal file
35
src/LibHac/Kvdb/AutoBuffer.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace LibHac.Kvdb
|
||||
{
|
||||
internal struct AutoBuffer : IDisposable
|
||||
{
|
||||
private const int Alignment = 0x10;
|
||||
|
||||
private MemoryResource _memoryResource;
|
||||
private MemoryResource.Buffer _buffer;
|
||||
|
||||
public Span<byte> Get() => _buffer.Get();
|
||||
public int GetSize() => _buffer.Length;
|
||||
|
||||
public Result Initialize(long size, MemoryResource memoryResource)
|
||||
{
|
||||
MemoryResource.Buffer buffer = memoryResource.Allocate(size, Alignment);
|
||||
if (!buffer.IsValid)
|
||||
return ResultKvdb.AllocationFailed.Log();
|
||||
|
||||
_memoryResource = memoryResource;
|
||||
_buffer = buffer;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_buffer.IsValid)
|
||||
{
|
||||
_memoryResource.Deallocate(ref _buffer, Alignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
src/LibHac/Kvdb/BoundedString.cs
Normal file
20
src/LibHac/Kvdb/BoundedString.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using LibHac.Common;
|
||||
|
||||
namespace LibHac.Kvdb
|
||||
{
|
||||
public struct BoundedString<TSize> where TSize : unmanaged
|
||||
{
|
||||
private TSize _string;
|
||||
|
||||
public Span<byte> Get() => SpanHelpers.AsByteSpan(ref _string);
|
||||
|
||||
public int GetLength() =>
|
||||
StringUtils.GetLength(SpanHelpers.AsReadOnlyByteSpan(in _string), Unsafe.SizeOf<TSize>());
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 768)]
|
||||
internal struct Size768 { }
|
||||
}
|
721
src/LibHac/Kvdb/FlatMapKeyValueStore.cs
Normal file
721
src/LibHac/Kvdb/FlatMapKeyValueStore.cs
Normal file
@ -0,0 +1,721 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
using LibHac.Fs;
|
||||
|
||||
namespace LibHac.Kvdb
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a collection of keys and values that are sorted by the key,
|
||||
/// and may be saved and loaded from an archive file on disk.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the keys in the keys in the key-value store.</typeparam>
|
||||
public class FlatMapKeyValueStore<TKey> : IDisposable where TKey : unmanaged, IEquatable<TKey>, IComparable<TKey>
|
||||
{
|
||||
private const int Alignment = 0x10;
|
||||
|
||||
private FileSystemClient _fsClient;
|
||||
private Index _index;
|
||||
private BoundedString<Size768> _archivePath;
|
||||
private MemoryResource _memoryResource;
|
||||
private MemoryResource _memoryResourceForAutoBuffers;
|
||||
|
||||
private static ReadOnlySpan<byte> ArchiveFileName => // /imkvdb.arc
|
||||
new[]
|
||||
{
|
||||
(byte) '/', (byte) 'i', (byte) 'm', (byte) 'k', (byte) 'v', (byte) 'd', (byte) 'b', (byte) '.',
|
||||
(byte) 'a', (byte) 'r', (byte) 'c'
|
||||
};
|
||||
|
||||
public int Count => _index.Count;
|
||||
|
||||
public FlatMapKeyValueStore()
|
||||
{
|
||||
_index = new Index();
|
||||
|
||||
Unsafe.SkipInit(out _archivePath);
|
||||
_archivePath.Get()[0] = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a <see cref="FlatMapKeyValueStore{T}"/>. Reads and writes the store to and from the file imkvdb.arc
|
||||
/// in the specified <paramref name="rootPath"/> directory. This directory must exist when calling <see cref="Initialize"/>,
|
||||
/// but it is not required for the imkvdb.arc file to exist.
|
||||
/// </summary>
|
||||
/// <param name="fsClient">The <see cref="FileSystemClient"/> to use for reading and writing the archive.</param>
|
||||
/// <param name="rootPath">The directory path used to load and save the archive file. Directory must already exist.</param>
|
||||
/// <param name="capacity">The maximum number of entries that can be stored.</param>
|
||||
/// <param name="memoryResource"><see cref="MemoryResource"/> for allocating buffers to hold entries and values.</param>
|
||||
/// <param name="autoBufferMemoryResource"><see cref="MemoryResource"/> for allocating temporary buffers
|
||||
/// when reading and writing the store to a file.</param>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
public Result Initialize(FileSystemClient fsClient, U8Span rootPath, int capacity,
|
||||
MemoryResource memoryResource, MemoryResource autoBufferMemoryResource)
|
||||
{
|
||||
// The root path must be an existing directory
|
||||
Result rc = fsClient.GetEntryType(out DirectoryEntryType rootEntryType, rootPath);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (rootEntryType == DirectoryEntryType.File)
|
||||
return ResultFs.PathNotFound.Log();
|
||||
|
||||
var sb = new U8StringBuilder(_archivePath.Get());
|
||||
sb.Append(rootPath).Append(ArchiveFileName);
|
||||
|
||||
rc = _index.Initialize(capacity, memoryResource);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
_fsClient = fsClient;
|
||||
_memoryResource = memoryResource;
|
||||
_memoryResourceForAutoBuffers = autoBufferMemoryResource;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_index.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all entries in the <see cref="FlatMapKeyValueStore{T}"/> and loads all entries
|
||||
/// from the database archive file, if it exists.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
public Result Load()
|
||||
{
|
||||
// Clear any existing entries.
|
||||
_index.Clear();
|
||||
|
||||
var buffer = new AutoBuffer();
|
||||
|
||||
try
|
||||
{
|
||||
Result rc = ReadArchive(ref buffer);
|
||||
if (rc.IsFailure())
|
||||
{
|
||||
// If the file is not found, we don't have any entries to load.
|
||||
if (ResultFs.PathNotFound.Includes(rc))
|
||||
return Result.Success.LogConverted(rc);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = LoadFrom(buffer.Get());
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
buffer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes all entries in the <see cref="FlatMapKeyValueStore{T}"/> to a database archive file.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
public Result Save()
|
||||
{
|
||||
// Create a buffer to hold the archive.
|
||||
var buffer = new AutoBuffer();
|
||||
Result rc = buffer.Initialize(CalculateArchiveSize(), _memoryResourceForAutoBuffers);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
try
|
||||
{
|
||||
// Write the archive to the buffer.
|
||||
Span<byte> span = buffer.Get();
|
||||
var writer = new KeyValueArchiveBufferWriter(span);
|
||||
SaveTo(ref writer);
|
||||
|
||||
// Save the buffer to disk.
|
||||
return CommitArchive(span);
|
||||
}
|
||||
finally
|
||||
{
|
||||
buffer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value associated with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="valueSize">If the method returns successfully, contains the size of
|
||||
/// the value written to <paramref name="valueBuffer"/>. This may be smaller than the
|
||||
/// actual length of the value if <paramref name="valueBuffer"/> was not large enough.</param>
|
||||
/// <param name="key">The key of the value to get.</param>
|
||||
/// <param name="valueBuffer">If the method returns successfully, contains the value
|
||||
/// associated with the specified key. Otherwise, the buffer will not be modified.</param>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
/// <remarks>Possible <see cref="Result"/>s:<br/>
|
||||
/// <see cref="ResultKvdb.KeyNotFound"/>
|
||||
/// The specified key was not found in the <see cref="FlatMapKeyValueStore{T}"/>.</remarks>
|
||||
public Result Get(out int valueSize, ref TKey key, Span<byte> valueBuffer)
|
||||
{
|
||||
Unsafe.SkipInit(out valueSize);
|
||||
|
||||
// Find entry.
|
||||
ConstIterator iterator = _index.GetLowerBoundConstIterator(ref key);
|
||||
if (iterator.IsEnd())
|
||||
return ResultKvdb.KeyNotFound.Log();
|
||||
|
||||
if (!key.Equals(iterator.Get().Key))
|
||||
return ResultKvdb.KeyNotFound.Log();
|
||||
|
||||
// Truncate the output if the buffer is too small.
|
||||
ReadOnlySpan<byte> value = iterator.GetValue();
|
||||
int size = Math.Min(valueBuffer.Length, value.Length);
|
||||
|
||||
value.Slice(0, size).CopyTo(valueBuffer);
|
||||
valueSize = size;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified key and value to the <see cref="FlatMapKeyValueStore{T}"/>.
|
||||
/// The existing value is replaced if the key already exists.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to add.</param>
|
||||
/// <param name="value">The value to add.</param>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
public Result Set(ref TKey key, ReadOnlySpan<byte> value)
|
||||
{
|
||||
return _index.Set(ref key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an element from the <see cref="FlatMapKeyValueStore{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to delete.</param>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
/// <remarks>Possible <see cref="Result"/>s:<br/>
|
||||
/// <see cref="ResultKvdb.KeyNotFound"/>
|
||||
/// The specified key was not found in the <see cref="FlatMapKeyValueStore{T}"/>.</remarks>
|
||||
public Result Delete(ref TKey key)
|
||||
{
|
||||
if (!_index.Delete(ref key))
|
||||
return ResultKvdb.KeyNotFound.Log();
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="Iterator"/> that starts at the first element in the <see cref="FlatMapKeyValueStore{T}"/>.
|
||||
/// </summary>
|
||||
/// <returns>The created iterator.</returns>
|
||||
public Iterator GetBeginIterator()
|
||||
{
|
||||
return _index.GetBeginIterator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="Iterator"/> that starts at the first element equal to or greater than
|
||||
/// <paramref name="key"/> in the <see cref="FlatMapKeyValueStore{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key at which to begin iteration.</param>
|
||||
/// <returns>The created iterator.</returns>
|
||||
public Iterator GetLowerBoundIterator(ref TKey key)
|
||||
{
|
||||
return _index.GetLowerBoundIterator(ref key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixes an iterator's current position and total length so that after an entry
|
||||
/// is added or removed, the iterator will still be on the same entry.
|
||||
/// </summary>
|
||||
/// <param name="iterator">The iterator to fix.</param>
|
||||
/// <param name="key">The key that was added or removed.</param>
|
||||
public void FixIterator(ref Iterator iterator, ref TKey key)
|
||||
{
|
||||
_index.FixIterator(ref iterator, ref key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the database archive file into the provided buffer.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer the file will be read into.</param>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
private Result ReadArchive(ref AutoBuffer buffer)
|
||||
{
|
||||
Result rc = _fsClient.OpenFile(out FileHandle file, new U8Span(_archivePath.Get()), OpenMode.Read);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
try
|
||||
{
|
||||
rc = _fsClient.GetFileSize(out long archiveSize, file);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = buffer.Initialize(archiveSize, _memoryResourceForAutoBuffers);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = _fsClient.ReadFile(file, 0, buffer.Get());
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fsClient.CloseFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads all key-value pairs from a key-value archive.
|
||||
/// All keys in the archive are assumed to be in ascending order.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer containing the key-value archive.</param>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
private Result LoadFrom(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
var reader = new KeyValueArchiveBufferReader(buffer);
|
||||
|
||||
Result rc = reader.ReadEntryCount(out int entryCount);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
for (int i = 0; i < entryCount; i++)
|
||||
{
|
||||
// Get size of key/value.
|
||||
rc = reader.GetKeyValueSize(out _, out int valueSize);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
// Allocate memory for value.
|
||||
MemoryResource.Buffer newValue = _memoryResource.Allocate(valueSize, Alignment);
|
||||
if (!newValue.IsValid)
|
||||
return ResultKvdb.AllocationFailed.Log();
|
||||
|
||||
bool success = false;
|
||||
try
|
||||
{
|
||||
// Read key and value.
|
||||
TKey key = default;
|
||||
|
||||
rc = reader.ReadKeyValue(SpanHelpers.AsByteSpan(ref key), newValue.Get());
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = _index.AppendUnsafe(ref key, newValue);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
success = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Deallocate the buffer if we didn't succeed.
|
||||
if (!success)
|
||||
_memoryResource.Deallocate(ref newValue, Alignment);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private void SaveTo(ref KeyValueArchiveBufferWriter writer)
|
||||
{
|
||||
writer.WriteHeader(_index.Count);
|
||||
|
||||
ConstIterator iterator = _index.GetBeginConstIterator();
|
||||
while (!iterator.IsEnd())
|
||||
{
|
||||
ReadOnlySpan<byte> key = SpanHelpers.AsReadOnlyByteSpan(in iterator.Get().Key);
|
||||
writer.WriteEntry(key, iterator.GetValue());
|
||||
|
||||
iterator.Next();
|
||||
}
|
||||
}
|
||||
|
||||
private Result CommitArchive(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
var path = new U8Span(_archivePath.Get());
|
||||
|
||||
// Try to delete the archive, but allow deletion failure.
|
||||
_fsClient.DeleteFile(path).IgnoreResult();
|
||||
|
||||
// Create new archive.
|
||||
Result rc = _fsClient.CreateFile(path, buffer.Length);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
// Write data to the archive.
|
||||
rc = _fsClient.OpenFile(out FileHandle file, path, OpenMode.Write);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
try
|
||||
{
|
||||
rc = _fsClient.WriteFile(file, 0, buffer, WriteOption.Flush);
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fsClient.CloseFile(file);
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private long CalculateArchiveSize()
|
||||
{
|
||||
var calculator = new KeyValueArchiveSizeCalculator();
|
||||
calculator.Initialize();
|
||||
ConstIterator iterator = _index.GetBeginConstIterator();
|
||||
|
||||
while (!iterator.IsEnd())
|
||||
{
|
||||
calculator.AddEntry(Unsafe.SizeOf<TKey>(), iterator.GetValue().Length);
|
||||
iterator.Next();
|
||||
}
|
||||
|
||||
return calculator.Size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a key-value pair contained in a <see cref="FlatMapKeyValueStore{T}"/>.
|
||||
/// </summary>
|
||||
public struct KeyValue
|
||||
{
|
||||
public TKey Key;
|
||||
public MemoryResource.Buffer Value;
|
||||
|
||||
public KeyValue(ref TKey key, MemoryResource.Buffer value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages the sorted list of <see cref="KeyValue"/> entries in a <see cref="FlatMapKeyValueStore{T}"/>.
|
||||
/// </summary>
|
||||
private struct Index : IDisposable
|
||||
{
|
||||
private int _count;
|
||||
private int _capacity;
|
||||
private KeyValue[] _entries;
|
||||
private MemoryResource _memoryResource;
|
||||
|
||||
/// <summary>
|
||||
/// The number of elements currently in the <see cref="Index"/>.
|
||||
/// </summary>
|
||||
public int Count => _count;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the <see cref="Index"/>
|
||||
/// </summary>
|
||||
/// <param name="capacity">The maximum number of elements the <see cref="Index"/> will be able to hold.</param>
|
||||
/// <param name="memoryResource">The <see cref="MemoryResource"/> that will be used to allocate
|
||||
/// memory for values added to the <see cref="Index"/>.</param>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
public Result Initialize(int capacity, MemoryResource memoryResource)
|
||||
{
|
||||
// Initialize must only be called once.
|
||||
Assert.Null(_entries);
|
||||
Assert.NotNull(memoryResource);
|
||||
|
||||
// FS uses the provided MemoryResource to allocate the KeyValue array.
|
||||
// We can't do that here because the array will contain managed references.
|
||||
_entries = new KeyValue[capacity];
|
||||
_capacity = capacity;
|
||||
_memoryResource = memoryResource;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_entries != null)
|
||||
{
|
||||
Clear();
|
||||
_entries = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified key and value to the <see cref="Index"/>.
|
||||
/// The existing value is replaced if the key already exists.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to add.</param>
|
||||
/// <param name="value">The value to add.</param>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
public Result Set(ref TKey key, ReadOnlySpan<byte> value)
|
||||
{
|
||||
// The list is sorted by key. Find the index to insert at.
|
||||
int index = GetLowerBoundIndex(ref key);
|
||||
|
||||
if (index != _count && _entries[index].Key.Equals(key))
|
||||
{
|
||||
// Key already exists. Free the old value.
|
||||
_memoryResource.Deallocate(ref _entries[index].Value, Alignment);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to insert a new entry. Check if there's room and shift the existing entries.
|
||||
if (_count >= _capacity)
|
||||
return ResultKvdb.OutOfKeyResource.Log();
|
||||
|
||||
Array.Copy(_entries, index, _entries, index + 1, _count - index);
|
||||
_count++;
|
||||
}
|
||||
|
||||
// Allocate new value.
|
||||
MemoryResource.Buffer newValue = _memoryResource.Allocate(value.Length, Alignment);
|
||||
if (!newValue.IsValid)
|
||||
return ResultKvdb.AllocationFailed.Log();
|
||||
|
||||
value.CopyTo(newValue.Get());
|
||||
|
||||
// Add the new entry to the list.
|
||||
_entries[index] = new KeyValue(ref key, newValue);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified key and value to the end of the list.
|
||||
/// Does not verify that the list will be sorted properly. The caller must make sure the sorting will be correct.
|
||||
/// Used when populating a new <see cref="Index"/> with already sorted entries.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to add.</param>
|
||||
/// <param name="value">The value to add.</param>
|
||||
/// <returns>The <see cref="Result"/> of the operation.</returns>
|
||||
public Result AppendUnsafe(ref TKey key, MemoryResource.Buffer value)
|
||||
{
|
||||
if (_count >= _capacity)
|
||||
return ResultKvdb.OutOfKeyResource.Log();
|
||||
|
||||
if (_count > 0)
|
||||
{
|
||||
// The key being added must be greater than the last key in the list.
|
||||
Assert.AssertTrue(key.CompareTo(_entries[_count - 1].Key) > 0);
|
||||
}
|
||||
|
||||
_entries[_count] = new KeyValue(ref key, value);
|
||||
_count++;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all keys and values from the <see cref="Index"/>.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Span<KeyValue> entries = _entries.AsSpan(0, _count);
|
||||
|
||||
for (int i = 0; i < entries.Length; i++)
|
||||
{
|
||||
_memoryResource.Deallocate(ref entries[i].Value, Alignment);
|
||||
}
|
||||
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an element from the <see cref="Index"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the element to delete.</param>
|
||||
/// <returns><see langword="true"/> if the item was found and deleted.
|
||||
/// <see langword="false"/> if the key was not in the store.</returns>
|
||||
public bool Delete(ref TKey key)
|
||||
{
|
||||
int index = GetLowerBoundIndex(ref key);
|
||||
|
||||
// Make sure the key was found.
|
||||
if (index == _count || !_entries[index].Key.Equals(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Free the value buffer and shift the remaining elements down
|
||||
_memoryResource.Deallocate(ref _entries[index].Value, Alignment);
|
||||
|
||||
Array.Copy(_entries, index + 1, _entries, index, _count - (index + 1));
|
||||
_count--;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an iterator starting at the first element in the <see cref="Index"/>.
|
||||
/// </summary>
|
||||
/// <returns>The created iterator.</returns>
|
||||
public Iterator GetBeginIterator()
|
||||
{
|
||||
return new Iterator(_entries, 0, _count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a read-only iterator starting at the first element in the <see cref="Index"/>.
|
||||
/// </summary>
|
||||
/// <returns>The created iterator.</returns>
|
||||
public ConstIterator GetBeginConstIterator()
|
||||
{
|
||||
return new ConstIterator(_entries, 0, _count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an iterator starting at the first element greater than or equal to <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key at which to begin iteration.</param>
|
||||
/// <returns>The created iterator.</returns>
|
||||
public Iterator GetLowerBoundIterator(ref TKey key)
|
||||
{
|
||||
int index = GetLowerBoundIndex(ref key);
|
||||
|
||||
return new Iterator(_entries, index, _count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a read-only iterator starting at the first element greater than or equal to <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key at which to begin iteration.</param>
|
||||
/// <returns>The created iterator.</returns>
|
||||
public ConstIterator GetLowerBoundConstIterator(ref TKey key)
|
||||
{
|
||||
int index = GetLowerBoundIndex(ref key);
|
||||
|
||||
return new ConstIterator(_entries, index, _count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixes an iterator's current position and total length so that after an entry
|
||||
/// is added or removed, the iterator will still be on the same entry.
|
||||
/// </summary>
|
||||
/// <param name="iterator">The iterator to fix.</param>
|
||||
/// <param name="key">The key that was added or removed.</param>
|
||||
public void FixIterator(ref Iterator iterator, ref TKey key)
|
||||
{
|
||||
int keyIndex = GetLowerBoundIndex(ref key);
|
||||
iterator.Fix(keyIndex, _count);
|
||||
}
|
||||
|
||||
private int GetLowerBoundIndex(ref TKey key)
|
||||
{
|
||||
// The AsSpan takes care of any bounds checking
|
||||
ReadOnlySpan<KeyValue> entries = _entries.AsSpan(0, _count);
|
||||
|
||||
return BinarySearch(ref MemoryMarshal.GetReference(entries), entries.Length, ref key);
|
||||
}
|
||||
|
||||
private static int BinarySearch(ref KeyValue spanStart, int length, ref TKey item)
|
||||
{
|
||||
// A tweaked version of .NET's SpanHelpers.BinarySearch
|
||||
int lo = 0;
|
||||
int hi = length - 1;
|
||||
|
||||
while (lo <= hi)
|
||||
{
|
||||
int i = (int)(((uint)hi + (uint)lo) >> 1);
|
||||
|
||||
int c = item.CompareTo(Unsafe.Add(ref spanStart, i).Key);
|
||||
if (c == 0)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
else if (c > 0)
|
||||
{
|
||||
lo = i + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
hi = i - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, return the index of the first element that is greater than item
|
||||
return lo;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates through the elements in a <see cref="FlatMapKeyValueStore{TKey}"/>.
|
||||
/// </summary>
|
||||
public struct Iterator
|
||||
{
|
||||
private KeyValue[] _entries;
|
||||
private int _index;
|
||||
private int _length;
|
||||
|
||||
internal Iterator(KeyValue[] entries, int startIndex, int length)
|
||||
{
|
||||
_entries = entries;
|
||||
_index = startIndex;
|
||||
_length = length;
|
||||
}
|
||||
|
||||
public ref KeyValue Get() => ref _entries[_index];
|
||||
public Span<byte> GetValue() => _entries[_index].Value.Get();
|
||||
|
||||
public void Next() => _index++;
|
||||
public bool IsEnd() => _index == _length;
|
||||
|
||||
/// <summary>
|
||||
/// Fixes the iterator current position and total length so that after an entry
|
||||
/// is added or removed, the iterator will still be on the same entry.
|
||||
/// </summary>
|
||||
/// <param name="entryIndex">The index of the added or removed entry.</param>
|
||||
/// <param name="newLength">The new length of the list.</param>
|
||||
/// <remarks></remarks>
|
||||
public void Fix(int entryIndex, int newLength)
|
||||
{
|
||||
if (newLength > _length)
|
||||
{
|
||||
// An entry was added. entryIndex is the index of the new entry.
|
||||
|
||||
// Only one entry can be added at a time.
|
||||
Assert.Equal(newLength, _length + 1);
|
||||
|
||||
if (entryIndex <= _index)
|
||||
{
|
||||
// The new entry was added at or before the iterator's current index.
|
||||
// Increment the index so we continue to be on the same entry.
|
||||
_index++;
|
||||
}
|
||||
|
||||
_length = newLength;
|
||||
}
|
||||
else if (newLength < _length)
|
||||
{
|
||||
// An entry was removed. entryIndex is the index where the removed entry used to be.
|
||||
|
||||
// Only one entry can be removed at a time.
|
||||
Assert.Equal(newLength, _length - 1);
|
||||
|
||||
if (entryIndex < _index)
|
||||
{
|
||||
// The removed entry was before the iterator's current index.
|
||||
// Decrement the index so we continue to be on the same entry.
|
||||
// If the entry at the iterator's current index was removed,
|
||||
// the iterator will now be at the next entry.
|
||||
_index--;
|
||||
}
|
||||
|
||||
_length = newLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates through the elements in a <see cref="FlatMapKeyValueStore{TKey}"/>.
|
||||
/// </summary>
|
||||
public struct ConstIterator
|
||||
{
|
||||
private KeyValue[] _entries;
|
||||
private int _index;
|
||||
private int _length;
|
||||
|
||||
public ConstIterator(KeyValue[] entries, int startIndex, int length)
|
||||
{
|
||||
_entries = entries;
|
||||
_index = startIndex;
|
||||
_length = length;
|
||||
}
|
||||
|
||||
public ref readonly KeyValue Get() => ref _entries[_index];
|
||||
public ReadOnlySpan<byte> GetValue() => _entries[_index].Value.Get();
|
||||
|
||||
public void Next() => _index++;
|
||||
public bool IsEnd() => _index == _length;
|
||||
}
|
||||
}
|
||||
}
|
207
src/LibHac/Kvdb/KeyValueArchive.cs
Normal file
207
src/LibHac/Kvdb/KeyValueArchive.cs
Normal file
@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
|
||||
namespace LibHac.Kvdb
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0xC)]
|
||||
public struct KeyValueArchiveHeader
|
||||
{
|
||||
public const uint ExpectedMagic = 0x564B4D49; // IMKV
|
||||
|
||||
public uint Magic;
|
||||
public int Reserved;
|
||||
public int EntryCount;
|
||||
|
||||
public bool IsValid() => Magic == ExpectedMagic;
|
||||
|
||||
public KeyValueArchiveHeader(int entryCount)
|
||||
{
|
||||
Magic = ExpectedMagic;
|
||||
Reserved = 0;
|
||||
EntryCount = entryCount;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0xC)]
|
||||
internal struct KeyValueArchiveEntryHeader
|
||||
{
|
||||
public const uint ExpectedMagic = 0x4E454D49; // IMEN
|
||||
|
||||
public uint Magic;
|
||||
public int KeySize;
|
||||
public int ValueSize;
|
||||
|
||||
public bool IsValid() => Magic == ExpectedMagic;
|
||||
|
||||
public KeyValueArchiveEntryHeader(int keySize, int valueSize)
|
||||
{
|
||||
Magic = ExpectedMagic;
|
||||
KeySize = keySize;
|
||||
ValueSize = valueSize;
|
||||
}
|
||||
}
|
||||
|
||||
internal struct KeyValueArchiveSizeCalculator
|
||||
{
|
||||
public long Size { get; private set; }
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
Size = Unsafe.SizeOf<KeyValueArchiveHeader>();
|
||||
}
|
||||
|
||||
public void AddEntry(int keySize, int valueSize)
|
||||
{
|
||||
Size += Unsafe.SizeOf<KeyValueArchiveEntryHeader>() + keySize + valueSize;
|
||||
}
|
||||
}
|
||||
|
||||
internal ref struct KeyValueArchiveBufferReader
|
||||
{
|
||||
private ReadOnlySpan<byte> _buffer;
|
||||
private int _offset;
|
||||
|
||||
public KeyValueArchiveBufferReader(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
public Result ReadEntryCount(out int count)
|
||||
{
|
||||
Unsafe.SkipInit(out count);
|
||||
|
||||
// This should only be called at the start of reading stream.
|
||||
Assert.AssertTrue(_offset == 0);
|
||||
|
||||
// Read and validate header.
|
||||
var header = new KeyValueArchiveHeader();
|
||||
|
||||
Result rc = Read(SpanHelpers.AsByteSpan(ref header));
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (!header.IsValid())
|
||||
return ResultKvdb.InvalidKeyValue.Log();
|
||||
|
||||
count = header.EntryCount;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public Result GetKeyValueSize(out int keySize, out int valueSize)
|
||||
{
|
||||
Unsafe.SkipInit(out keySize);
|
||||
Unsafe.SkipInit(out valueSize);
|
||||
|
||||
// This should only be called after ReadEntryCount.
|
||||
Assert.NotEqual(_offset, 0);
|
||||
|
||||
// Peek the next entry header.
|
||||
KeyValueArchiveEntryHeader header = default;
|
||||
|
||||
Result rc = Peek(SpanHelpers.AsByteSpan(ref header));
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (!header.IsValid())
|
||||
return ResultKvdb.InvalidKeyValue.Log();
|
||||
|
||||
keySize = header.KeySize;
|
||||
valueSize = header.ValueSize;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public Result ReadKeyValue(Span<byte> keyBuffer, Span<byte> valueBuffer)
|
||||
{
|
||||
// This should only be called after ReadEntryCount.
|
||||
Assert.NotEqual(_offset, 0);
|
||||
|
||||
// Read the next entry header.
|
||||
KeyValueArchiveEntryHeader header = default;
|
||||
|
||||
Result rc = Read(SpanHelpers.AsByteSpan(ref header));
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (!header.IsValid())
|
||||
return ResultKvdb.InvalidKeyValue.Log();
|
||||
|
||||
// Key size and Value size must be correct.
|
||||
Assert.Equal(keyBuffer.Length, header.KeySize);
|
||||
Assert.Equal(valueBuffer.Length, header.ValueSize);
|
||||
|
||||
rc = Read(keyBuffer);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = Read(valueBuffer);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private Result Peek(Span<byte> destBuffer)
|
||||
{
|
||||
// Bounds check.
|
||||
if (_offset + destBuffer.Length > _buffer.Length ||
|
||||
_offset + destBuffer.Length <= _offset)
|
||||
{
|
||||
return ResultKvdb.InvalidKeyValue.Log();
|
||||
}
|
||||
|
||||
_buffer.Slice(_offset, destBuffer.Length).CopyTo(destBuffer);
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private Result Read(Span<byte> destBuffer)
|
||||
{
|
||||
Result rc = Peek(destBuffer);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
_offset += destBuffer.Length;
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
internal ref struct KeyValueArchiveBufferWriter
|
||||
{
|
||||
private Span<byte> _buffer;
|
||||
private int _offset;
|
||||
|
||||
public KeyValueArchiveBufferWriter(Span<byte> buffer)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
private void Write(ReadOnlySpan<byte> source)
|
||||
{
|
||||
// Bounds check.
|
||||
Assert.AssertTrue(_offset + source.Length <= _buffer.Length &&
|
||||
_offset + source.Length > _offset);
|
||||
|
||||
source.CopyTo(_buffer.Slice(_offset));
|
||||
_offset += source.Length;
|
||||
}
|
||||
|
||||
public void WriteHeader(int entryCount)
|
||||
{
|
||||
// This should only be called at start of write.
|
||||
Assert.Equal(_offset, 0);
|
||||
|
||||
var header = new KeyValueArchiveHeader(entryCount);
|
||||
Write(SpanHelpers.AsByteSpan(ref header));
|
||||
}
|
||||
|
||||
public void WriteEntry(ReadOnlySpan<byte> key, ReadOnlySpan<byte> value)
|
||||
{
|
||||
// This should only be called after writing header.
|
||||
Assert.NotEqual(_offset, 0);
|
||||
|
||||
var header = new KeyValueArchiveEntryHeader(key.Length, value.Length);
|
||||
Write(SpanHelpers.AsByteSpan(ref header));
|
||||
Write(key);
|
||||
Write(value);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,8 +15,8 @@ namespace LibHac.Kvdb
|
||||
{
|
||||
public const int ModuleKvdb = 20;
|
||||
|
||||
/// <summary>Error code: 2020-0001; Inner value: 0x214</summary>
|
||||
public static Result.Base TooLargeKeyOrDbFull => new Result.Base(ModuleKvdb, 1);
|
||||
/// <summary>There is no more space in the database or the key is too long.<br/>Error code: 2020-0001; Inner value: 0x214</summary>
|
||||
public static Result.Base OutOfKeyResource => new Result.Base(ModuleKvdb, 1);
|
||||
/// <summary>Error code: 2020-0002; Inner value: 0x414</summary>
|
||||
public static Result.Base KeyNotFound => new Result.Base(ModuleKvdb, 2);
|
||||
/// <summary>Error code: 2020-0004; Inner value: 0x814</summary>
|
||||
|
@ -44,6 +44,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0-preview.6.20305.6" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
93
src/LibHac/MemoryResource.cs
Normal file
93
src/LibHac/MemoryResource.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace LibHac
|
||||
{
|
||||
public abstract class MemoryResource
|
||||
{
|
||||
private const int DefaultAlignment = 8;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Buffer Allocate(long size, int alignment = DefaultAlignment) =>
|
||||
DoAllocate(size, alignment);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Deallocate(ref Buffer buffer, int alignment = DefaultAlignment)
|
||||
{
|
||||
DoDeallocate(buffer, alignment);
|
||||
|
||||
// Clear the references to the deallocated buffer.
|
||||
buffer = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsEqual(MemoryResource other) =>
|
||||
DoIsEqual(other);
|
||||
|
||||
protected abstract Buffer DoAllocate(long size, int alignment);
|
||||
protected abstract void DoDeallocate(Buffer buffer, int alignment);
|
||||
protected abstract bool DoIsEqual(MemoryResource other);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a region of memory allocated by a <see cref="MemoryResource"/>.
|
||||
/// </summary>
|
||||
public struct Buffer
|
||||
{
|
||||
private Memory<byte> _memory;
|
||||
|
||||
/// <summary>
|
||||
/// A field where <see cref="MemoryResource"/> implementers can store info about the <see cref="Buffer"/>.
|
||||
/// </summary>
|
||||
internal object Extra { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of the buffer in bytes.
|
||||
/// </summary>
|
||||
public readonly int Length => _memory.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a span from the <see cref="Buffer"/>.
|
||||
/// </summary>
|
||||
public readonly Span<byte> Get() => _memory.Span;
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the <see cref="Buffer"/> is valid.
|
||||
/// </summary>
|
||||
public readonly bool IsValid => !_memory.Equals(default);
|
||||
|
||||
internal Buffer(Memory<byte> memory, object extra = null)
|
||||
{
|
||||
_memory = memory;
|
||||
Extra = extra;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ArrayPoolMemoryResource : MemoryResource
|
||||
{
|
||||
protected override Buffer DoAllocate(long size, int alignment)
|
||||
{
|
||||
byte[] array = ArrayPool<byte>.Shared.Rent((int)size);
|
||||
|
||||
return new Buffer(array.AsMemory(0, (int)size), array);
|
||||
}
|
||||
|
||||
protected override void DoDeallocate(Buffer buffer, int alignment)
|
||||
{
|
||||
if (buffer.Extra is byte[] array)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(array);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new LibHacException("Buffer was not allocated by this MemoryResource.");
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool DoIsEqual(MemoryResource other)
|
||||
{
|
||||
return ReferenceEquals(this, other);
|
||||
}
|
||||
}
|
||||
}
|
54
tests/LibHac.Tests/FullCycleRandom.cs
Normal file
54
tests/LibHac.Tests/FullCycleRandom.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LibHac.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple, full-cycle PRNG for use in tests.
|
||||
/// </summary>
|
||||
public class FullCycleRandom
|
||||
{
|
||||
private int _state;
|
||||
private int _mult;
|
||||
private int _inc;
|
||||
private int _and;
|
||||
private int _max;
|
||||
|
||||
public FullCycleRandom(int period, int seed)
|
||||
{
|
||||
// Avoid exponential growth pattern when initializing with a 0 seed
|
||||
seed ^= 0x55555555;
|
||||
_max = period - 1;
|
||||
int order = BitOperations.Log2((uint)period - 1) + 1;
|
||||
|
||||
// There isn't any deep reasoning behind the choice of the number of bits
|
||||
// in the seed used for initializing each parameter
|
||||
int multSeedBits = Math.Max(order >> 1, 2);
|
||||
int multSeedMask = (1 << multSeedBits) - 1;
|
||||
int multSeed = seed & multSeedMask;
|
||||
_mult = (multSeed << 2) | 5;
|
||||
|
||||
int incSeedBits = Math.Max(order >> 2, 2);
|
||||
int incSeedMask = (1 << incSeedBits) - 1;
|
||||
int incSeed = (seed >> multSeedBits) & incSeedMask;
|
||||
_inc = incSeed | 1;
|
||||
|
||||
int stateSeedBits = order;
|
||||
int stateSeedMask = (1 << stateSeedBits) - 1;
|
||||
int stateSeed = (seed >> multSeedBits + incSeedBits) & stateSeedMask;
|
||||
_state = stateSeed;
|
||||
|
||||
_and = (1 << order) - 1;
|
||||
}
|
||||
|
||||
public int Next()
|
||||
{
|
||||
do
|
||||
{
|
||||
_state = (_state * _mult + _inc) & _and;
|
||||
} while (_state > _max);
|
||||
|
||||
return _state;
|
||||
}
|
||||
}
|
||||
}
|
584
tests/LibHac.Tests/Kvdb/FlatMapKeyValueStoreTests.cs
Normal file
584
tests/LibHac.Tests/Kvdb/FlatMapKeyValueStoreTests.cs
Normal file
@ -0,0 +1,584 @@
|
||||
using System;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Kvdb;
|
||||
using LibHac.Tests.Fs.FileSystemClientTests;
|
||||
using Xunit;
|
||||
|
||||
using TTest = System.Int32;
|
||||
|
||||
namespace LibHac.Tests.Kvdb
|
||||
{
|
||||
public class FlatMapKeyValueStoreTests
|
||||
{
|
||||
private static readonly U8String MountName = new U8String("mount");
|
||||
private static readonly U8String RootPath = new U8String("mount:/");
|
||||
private static readonly U8String ArchiveFilePath = new U8String("mount:/imkvdb.arc");
|
||||
|
||||
private static (FlatMapKeyValueStore<T> kvStore, FileSystemClient fsClient) Create<T>(int capacity)
|
||||
where T : unmanaged, IEquatable<T>, IComparable<T>
|
||||
{
|
||||
FileSystemClient fsClient = FileSystemServerFactory.CreateClient(false);
|
||||
|
||||
var mountedFs = new InMemoryFileSystem();
|
||||
fsClient.Register(MountName, mountedFs).ThrowIfFailure();
|
||||
|
||||
FlatMapKeyValueStore<T> kvStore = Create<T>(fsClient, capacity);
|
||||
|
||||
return (kvStore, fsClient);
|
||||
}
|
||||
|
||||
private static FlatMapKeyValueStore<T> Create<T>(FileSystemClient fsClient, int capacity)
|
||||
where T : unmanaged, IEquatable<T>, IComparable<T>
|
||||
{
|
||||
var memoryResource = new ArrayPoolMemoryResource();
|
||||
|
||||
var kvStore = new FlatMapKeyValueStore<T>();
|
||||
kvStore.Initialize(fsClient, RootPath, capacity, memoryResource, memoryResource).ThrowIfFailure();
|
||||
|
||||
return kvStore;
|
||||
}
|
||||
|
||||
private static byte[][] GenerateValues(int count, int startingSize)
|
||||
{
|
||||
var values = new byte[count][];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var value = new byte[startingSize + i];
|
||||
value.AsSpan().Fill((byte)count);
|
||||
values[i] = value;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static Result PopulateKvStore(FlatMapKeyValueStore<TTest> kvStore, out byte[][] addedValues, int count,
|
||||
int startingValueSize = 20, int seed = -1)
|
||||
{
|
||||
addedValues = null;
|
||||
byte[][] values = GenerateValues(count, startingValueSize);
|
||||
|
||||
if (seed == -1)
|
||||
{
|
||||
for (TTest i = 0; i < count; i++)
|
||||
{
|
||||
Result rc = kvStore.Set(ref i, values[i]);
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var rng = new FullCycleRandom(count, seed);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
TTest index = rng.Next();
|
||||
Result rc = kvStore.Set(ref index, values[index]);
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
}
|
||||
|
||||
addedValues = values;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Count_EmptyStore_ReturnsZero()
|
||||
{
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(10);
|
||||
|
||||
Assert.Equal(0, kvStore.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(5)]
|
||||
[InlineData(10)]
|
||||
public void Count_PopulatedStore_ReturnsCorrectCount(int count)
|
||||
{
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(10);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
Assert.Equal(count, kvStore.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_FileDoesNotExist_ExistingEntriesAreCleared()
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
Assert.Success(kvStore.Load());
|
||||
Assert.Equal(0, kvStore.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(10)]
|
||||
public void Load_AfterArchiveHasBeenSaved_AllEntriesAreLoaded(int count)
|
||||
{
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient fsClient) = Create<TTest>(count + 5);
|
||||
Assert.Success(PopulateKvStore(kvStore, out byte[][] values, count));
|
||||
|
||||
Assert.Success(kvStore.Save());
|
||||
kvStore.Dispose();
|
||||
|
||||
kvStore = Create<TTest>(fsClient, count + 5);
|
||||
Assert.Success(kvStore.Load());
|
||||
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetBeginIterator();
|
||||
|
||||
// Check if each key-value pair matches
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
TTest expectedKey = i;
|
||||
byte[] expectedValue = values[i];
|
||||
|
||||
ref FlatMapKeyValueStore<TTest>.KeyValue kv = ref iterator.Get();
|
||||
|
||||
Assert.Equal(expectedKey, kv.Key);
|
||||
Assert.Equal(expectedValue, kv.Value.Get().ToArray());
|
||||
|
||||
iterator.Next();
|
||||
}
|
||||
|
||||
Assert.True(iterator.IsEnd());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_CapacityIsTooSmall_ReturnsOutOfKeyResource()
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient fsClient) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
Assert.Success(kvStore.Save());
|
||||
kvStore.Dispose();
|
||||
|
||||
|
||||
kvStore = Create<TTest>(fsClient, count - 5);
|
||||
Assert.Result(ResultKvdb.OutOfKeyResource, kvStore.Load());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(10)]
|
||||
public void Save_ArchiveFileIsWrittenToDisk(int count)
|
||||
{
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient fsClient) = Create<TTest>(count + 5);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
Assert.Success(kvStore.Save());
|
||||
|
||||
Assert.Success(fsClient.GetEntryType(out DirectoryEntryType entryType, ArchiveFilePath));
|
||||
Assert.Equal(DirectoryEntryType.File, entryType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_PopulatedStoreAndEntryDoesNotExist_ReturnsKeyNotFound()
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
TTest key = 20;
|
||||
var value = new byte[20];
|
||||
|
||||
Result rc = kvStore.Get(out int _, ref key, value);
|
||||
Assert.Result(ResultKvdb.KeyNotFound, rc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_PopulatedStore_GetsCorrectValueSizes()
|
||||
{
|
||||
const int count = 10;
|
||||
const int startingValueSize = 20;
|
||||
const int rngSeed = 220;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count, startingValueSize, rngSeed));
|
||||
|
||||
// Check the size of each entry
|
||||
var value = new byte[100];
|
||||
|
||||
for (TTest i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Success(kvStore.Get(out int valueSize, ref i, value));
|
||||
Assert.Equal(startingValueSize + i, valueSize);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_PopulatedStoreAndEntryExists_GetsCorrectValue()
|
||||
{
|
||||
const int count = 10;
|
||||
const int startingValueSize = 20;
|
||||
const int rngSeed = 188;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out byte[][] values, count, startingValueSize, rngSeed));
|
||||
|
||||
// Check if each value matches
|
||||
var value = new byte[100];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
TTest key = i;
|
||||
Assert.Success(kvStore.Get(out int _, ref key, value));
|
||||
Assert.Equal(values[i], value.AsSpan(0, startingValueSize + i).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(5)]
|
||||
[InlineData(10)]
|
||||
public void Set_StoreIsFullAndEntryDoesNotExist_ReturnsOutOfKeyResource(int count)
|
||||
{
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out byte[][] values, count));
|
||||
|
||||
TTest key = count;
|
||||
Result rc = kvStore.Set(ref key, values[0]);
|
||||
|
||||
Assert.Result(ResultKvdb.OutOfKeyResource, rc);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(5)]
|
||||
[InlineData(9)]
|
||||
public void Set_StoreIsFullAndEntryAlreadyExists_ReplacesOriginalValue(int entryToReplace)
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
TTest key = entryToReplace;
|
||||
var value = new byte[15];
|
||||
value.AsSpan().Fill(0xFF);
|
||||
|
||||
Assert.Success(kvStore.Set(ref key, value));
|
||||
|
||||
// Read back the value
|
||||
var readValue = new byte[20];
|
||||
Assert.Success(kvStore.Get(out int valueSize, ref key, readValue));
|
||||
|
||||
// Check the value contents and size
|
||||
Assert.Equal(value.Length, valueSize);
|
||||
Assert.Equal(value, readValue.AsSpan(0, valueSize).ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(10, 89)]
|
||||
[InlineData(10, 50)]
|
||||
[InlineData(1000, 75367)]
|
||||
[InlineData(1000, 117331)]
|
||||
public void Set_StoreIsFilledInRandomOrder_EntriesAreSorted(int entryCount, int rngSeed)
|
||||
{
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(entryCount + 10);
|
||||
Assert.Success(PopulateKvStore(kvStore, out byte[][] values, entryCount, 20, rngSeed));
|
||||
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetBeginIterator();
|
||||
|
||||
// Check if each key-value pair matches
|
||||
for (int i = 0; i < entryCount; i++)
|
||||
{
|
||||
TTest expectedKey = i;
|
||||
byte[] expectedValue = values[i];
|
||||
|
||||
ref FlatMapKeyValueStore<TTest>.KeyValue kv = ref iterator.Get();
|
||||
|
||||
Assert.Equal(expectedKey, kv.Key);
|
||||
Assert.Equal(expectedValue, kv.Value.Get().ToArray());
|
||||
|
||||
iterator.Next();
|
||||
}
|
||||
|
||||
Assert.True(iterator.IsEnd());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delete_EmptyStore_ReturnsKeyNotFound()
|
||||
{
|
||||
const int count = 10;
|
||||
TTest keyToDelete = 4;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
|
||||
Result rc = kvStore.Delete(ref keyToDelete);
|
||||
Assert.Result(ResultKvdb.KeyNotFound, rc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delete_PopulatedStoreAndEntryDoesNotExist_ReturnsKeyNotFound()
|
||||
{
|
||||
const int count = 10;
|
||||
TTest keyToDelete = 44;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
Result rc = kvStore.Delete(ref keyToDelete);
|
||||
Assert.Result(ResultKvdb.KeyNotFound, rc);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(5)]
|
||||
[InlineData(9)]
|
||||
public void Delete_PopulatedStoreAndEntryExists_CannotGetAfterDeletion(int entryToDelete)
|
||||
{
|
||||
const int count = 10;
|
||||
const int startingValueSize = 20;
|
||||
const int rngSeed = 114;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count, startingValueSize, rngSeed));
|
||||
|
||||
TTest keyToDelete = entryToDelete;
|
||||
Assert.Success(kvStore.Delete(ref keyToDelete));
|
||||
|
||||
var value = new byte[20];
|
||||
|
||||
Result rc = kvStore.Get(out int _, ref keyToDelete, value);
|
||||
Assert.Result(ResultKvdb.KeyNotFound, rc);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(5)]
|
||||
[InlineData(9)]
|
||||
public void Delete_PopulatedStoreAndEntryExists_CountIsDecremented(int entryToDelete)
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
TTest keyToDelete = entryToDelete;
|
||||
Assert.Success(kvStore.Delete(ref keyToDelete));
|
||||
|
||||
Assert.Equal(count - 1, kvStore.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(5)]
|
||||
[InlineData(9)]
|
||||
public void Delete_PopulatedStoreAndEntryExists_RemainingEntriesAreSorted(int entryToDelete)
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
TTest keyToDelete = entryToDelete;
|
||||
Assert.Success(kvStore.Delete(ref keyToDelete));
|
||||
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetBeginIterator();
|
||||
|
||||
// Check if the remaining keys exist in order
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (i == entryToDelete)
|
||||
continue;
|
||||
|
||||
TTest expectedKey = i;
|
||||
|
||||
Assert.Equal(expectedKey, iterator.Get().Key);
|
||||
|
||||
iterator.Next();
|
||||
}
|
||||
|
||||
Assert.True(iterator.IsEnd());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(5)]
|
||||
[InlineData(9)]
|
||||
public void GetLowerBoundIterator_EntryExists_StartsIterationAtSpecifiedKey(int startEntry)
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
TTest startingKey = startEntry;
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetLowerBoundIterator(ref startingKey);
|
||||
|
||||
Assert.False(iterator.IsEnd());
|
||||
Assert.Equal(startingKey, iterator.Get().Key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(5)]
|
||||
[InlineData(9)]
|
||||
public void GetLowerBoundIterator_EntryDoesNotExist_StartsIterationAtNextLargestKey(int startIndex)
|
||||
{
|
||||
const int count = 10;
|
||||
const int startingValueSize = 20;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
|
||||
byte[][] values = GenerateValues(count, startingValueSize);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
TTest key = i * 2;
|
||||
Assert.Success(kvStore.Set(ref key, values[i]));
|
||||
}
|
||||
|
||||
TTest startingKey = startIndex;
|
||||
TTest nextLargestKey = startIndex + 1;
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetLowerBoundIterator(ref startingKey);
|
||||
|
||||
Assert.False(iterator.IsEnd());
|
||||
Assert.Equal(nextLargestKey, iterator.Get().Key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLowerBoundIterator_LargerThanAllKeysInStore_IteratorIsAtEnd()
|
||||
{
|
||||
const int count = 10;
|
||||
const int startIndex = 20;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
TTest key = startIndex;
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetLowerBoundIterator(ref key);
|
||||
|
||||
Assert.True(iterator.IsEnd());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2, 3, 2)]
|
||||
[InlineData(3, 3, 4)]
|
||||
[InlineData(5, 3, 5)]
|
||||
public void FixIterator_RemoveEntry_IteratorPointsToSameEntry(int positionWhenRemoving, int entryToRemove, int expectedNewPosition)
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetBeginIterator();
|
||||
|
||||
while (iterator.Get().Key != positionWhenRemoving)
|
||||
{
|
||||
iterator.Next();
|
||||
}
|
||||
|
||||
TTest keyToRemove = entryToRemove;
|
||||
Assert.Success(kvStore.Delete(ref keyToRemove));
|
||||
|
||||
kvStore.FixIterator(ref iterator, ref keyToRemove);
|
||||
|
||||
TTest expectedKey = expectedNewPosition;
|
||||
Assert.Equal(expectedKey, iterator.Get().Key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(6, 7, 6)]
|
||||
[InlineData(8, 7, 8)]
|
||||
public void FixIterator_AddEntry_IteratorPointsToSameEntry(int positionWhenAdding, int entryToAdd, int expectedNewPosition)
|
||||
{
|
||||
const int count = 10;
|
||||
const int startingValueSize = 20;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count + 5);
|
||||
|
||||
byte[][] values = GenerateValues(count, startingValueSize);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
TTest key = i * 2;
|
||||
Assert.Success(kvStore.Set(ref key, values[i]));
|
||||
}
|
||||
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetBeginIterator();
|
||||
|
||||
while (iterator.Get().Key != positionWhenAdding)
|
||||
{
|
||||
iterator.Next();
|
||||
}
|
||||
|
||||
TTest keyToAdd = entryToAdd;
|
||||
var valueToAdd = new byte[10];
|
||||
|
||||
Assert.Success(kvStore.Set(ref keyToAdd, valueToAdd));
|
||||
|
||||
kvStore.FixIterator(ref iterator, ref keyToAdd);
|
||||
|
||||
TTest expectedKey = expectedNewPosition;
|
||||
Assert.Equal(expectedKey, iterator.Get().Key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IteratorIsEnd_EmptyStore_ReturnsTrue()
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetBeginIterator();
|
||||
Assert.True(iterator.IsEnd());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IteratorIsEnd_PopulatedStore_ReturnsFalseUntilFinishedIterating()
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out _, count));
|
||||
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetBeginIterator();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.False(iterator.IsEnd());
|
||||
iterator.Next();
|
||||
}
|
||||
|
||||
// Iterated all entries. Should return true now
|
||||
Assert.True(iterator.IsEnd());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IteratorGet_PopulatedStore_ReturnsEntriesInOrder()
|
||||
{
|
||||
const int count = 10;
|
||||
|
||||
(FlatMapKeyValueStore<TTest> kvStore, FileSystemClient _) = Create<TTest>(count);
|
||||
Assert.Success(PopulateKvStore(kvStore, out byte[][] values, count));
|
||||
|
||||
FlatMapKeyValueStore<TTest>.Iterator iterator = kvStore.GetBeginIterator();
|
||||
|
||||
// Check if each key-value pair matches
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
TTest expectedKey = i;
|
||||
byte[] expectedValue = values[i];
|
||||
|
||||
ref FlatMapKeyValueStore<TTest>.KeyValue kv = ref iterator.Get();
|
||||
|
||||
Assert.Equal(expectedKey, kv.Key);
|
||||
Assert.Equal(expectedValue, kv.Value.Get().ToArray());
|
||||
|
||||
iterator.Next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user