Add FlatMapKeyValueStore with tests

This commit is contained in:
Alex Barney 2020-07-18 19:39:20 -07:00
parent 672a0016b3
commit 37251968c0
12 changed files with 1729 additions and 5 deletions

View File

@ -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.

View File

@ -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.");
}

View File

@ -316,7 +316,7 @@ namespace LibHac.FsService
{
if (reader.Indexer.IsFull())
{
return ResultKvdb.TooLargeKeyOrDbFull.Log();
return ResultKvdb.OutOfKeyResource.Log();
}
}

View 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);
}
}
}
}

View 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 { }
}

View 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;
}
}
}

View 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);
}
}
}

View File

@ -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>

View File

@ -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>

View 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);
}
}
}

View 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;
}
}
}

View 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();
}
}
}
}