diff --git a/build/CodeGen/results.csv b/build/CodeGen/results.csv index 677019fc..ab43adcb 100644 --- a/build/CodeGen/results.csv +++ b/build/CodeGen/results.csv @@ -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, diff --git a/src/LibHac/Diag/Assert.cs b/src/LibHac/Diag/Assert.cs index af40976c..1373fa7e 100644 --- a/src/LibHac/Diag/Assert.cs +++ b/src/LibHac/Diag/Assert.cs @@ -21,9 +21,18 @@ namespace LibHac.Diag } [Conditional("DEBUG")] - public static void NotNull([NotNull] T item) where T : class + public static void Null([NotNull] T item) where T : class { if (!(item is null)) + { + throw new LibHacException("Null assertion failed."); + } + } + + [Conditional("DEBUG")] + public static void NotNull([NotNull] T item) where T : class + { + if (item is null) { throw new LibHacException("Not-null assertion failed."); } diff --git a/src/LibHac/FsService/FileSystemProxy.cs b/src/LibHac/FsService/FileSystemProxy.cs index f73dbbb4..6358ac05 100644 --- a/src/LibHac/FsService/FileSystemProxy.cs +++ b/src/LibHac/FsService/FileSystemProxy.cs @@ -316,7 +316,7 @@ namespace LibHac.FsService { if (reader.Indexer.IsFull()) { - return ResultKvdb.TooLargeKeyOrDbFull.Log(); + return ResultKvdb.OutOfKeyResource.Log(); } } diff --git a/src/LibHac/Kvdb/AutoBuffer.cs b/src/LibHac/Kvdb/AutoBuffer.cs new file mode 100644 index 00000000..98dbd930 --- /dev/null +++ b/src/LibHac/Kvdb/AutoBuffer.cs @@ -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 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); + } + } + } +} diff --git a/src/LibHac/Kvdb/BoundedString.cs b/src/LibHac/Kvdb/BoundedString.cs new file mode 100644 index 00000000..a94ca2ce --- /dev/null +++ b/src/LibHac/Kvdb/BoundedString.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using LibHac.Common; + +namespace LibHac.Kvdb +{ + public struct BoundedString where TSize : unmanaged + { + private TSize _string; + + public Span Get() => SpanHelpers.AsByteSpan(ref _string); + + public int GetLength() => + StringUtils.GetLength(SpanHelpers.AsReadOnlyByteSpan(in _string), Unsafe.SizeOf()); + } + + [StructLayout(LayoutKind.Sequential, Size = 768)] + internal struct Size768 { } +} diff --git a/src/LibHac/Kvdb/FlatMapKeyValueStore.cs b/src/LibHac/Kvdb/FlatMapKeyValueStore.cs new file mode 100644 index 00000000..efe6dfb9 --- /dev/null +++ b/src/LibHac/Kvdb/FlatMapKeyValueStore.cs @@ -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 +{ + /// + /// 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. + /// + /// The type of the keys in the keys in the key-value store. + public class FlatMapKeyValueStore : IDisposable where TKey : unmanaged, IEquatable, IComparable + { + private const int Alignment = 0x10; + + private FileSystemClient _fsClient; + private Index _index; + private BoundedString _archivePath; + private MemoryResource _memoryResource; + private MemoryResource _memoryResourceForAutoBuffers; + + private static ReadOnlySpan 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; + } + + /// + /// Initializes a . Reads and writes the store to and from the file imkvdb.arc + /// in the specified directory. This directory must exist when calling , + /// but it is not required for the imkvdb.arc file to exist. + /// + /// The to use for reading and writing the archive. + /// The directory path used to load and save the archive file. Directory must already exist. + /// The maximum number of entries that can be stored. + /// for allocating buffers to hold entries and values. + /// for allocating temporary buffers + /// when reading and writing the store to a file. + /// The of the operation. + 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(); + } + + /// + /// Clears all entries in the and loads all entries + /// from the database archive file, if it exists. + /// + /// The of the operation. + 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(); + } + } + + /// + /// Writes all entries in the to a database archive file. + /// + /// The of the operation. + 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 span = buffer.Get(); + var writer = new KeyValueArchiveBufferWriter(span); + SaveTo(ref writer); + + // Save the buffer to disk. + return CommitArchive(span); + } + finally + { + buffer.Dispose(); + } + } + + /// + /// Gets the value associated with the specified key. + /// + /// If the method returns successfully, contains the size of + /// the value written to . This may be smaller than the + /// actual length of the value if was not large enough. + /// The key of the value to get. + /// If the method returns successfully, contains the value + /// associated with the specified key. Otherwise, the buffer will not be modified. + /// The of the operation. + /// Possible s:
+ /// + /// The specified key was not found in the .
+ public Result Get(out int valueSize, ref TKey key, Span 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 value = iterator.GetValue(); + int size = Math.Min(valueBuffer.Length, value.Length); + + value.Slice(0, size).CopyTo(valueBuffer); + valueSize = size; + return Result.Success; + } + + /// + /// Adds the specified key and value to the . + /// The existing value is replaced if the key already exists. + /// + /// The key to add. + /// The value to add. + /// The of the operation. + public Result Set(ref TKey key, ReadOnlySpan value) + { + return _index.Set(ref key, value); + } + + /// + /// Deletes an element from the . + /// + /// The key of the element to delete. + /// The of the operation. + /// Possible s:
+ /// + /// The specified key was not found in the .
+ public Result Delete(ref TKey key) + { + if (!_index.Delete(ref key)) + return ResultKvdb.KeyNotFound.Log(); + + return Result.Success; + } + + /// + /// Creates an that starts at the first element in the . + /// + /// The created iterator. + public Iterator GetBeginIterator() + { + return _index.GetBeginIterator(); + } + + /// + /// Creates an that starts at the first element equal to or greater than + /// in the . + /// + /// The key at which to begin iteration. + /// The created iterator. + public Iterator GetLowerBoundIterator(ref TKey key) + { + return _index.GetLowerBoundIterator(ref key); + } + + /// + /// 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. + /// + /// The iterator to fix. + /// The key that was added or removed. + public void FixIterator(ref Iterator iterator, ref TKey key) + { + _index.FixIterator(ref iterator, ref key); + } + + /// + /// Reads the database archive file into the provided buffer. + /// + /// The buffer the file will be read into. + /// The of the operation. + 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); + } + } + + /// + /// Loads all key-value pairs from a key-value archive. + /// All keys in the archive are assumed to be in ascending order. + /// + /// The buffer containing the key-value archive. + /// The of the operation. + private Result LoadFrom(ReadOnlySpan 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 key = SpanHelpers.AsReadOnlyByteSpan(in iterator.Get().Key); + writer.WriteEntry(key, iterator.GetValue()); + + iterator.Next(); + } + } + + private Result CommitArchive(ReadOnlySpan 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(), iterator.GetValue().Length); + iterator.Next(); + } + + return calculator.Size; + } + + /// + /// Represents a key-value pair contained in a . + /// + public struct KeyValue + { + public TKey Key; + public MemoryResource.Buffer Value; + + public KeyValue(ref TKey key, MemoryResource.Buffer value) + { + Key = key; + Value = value; + } + } + + /// + /// Manages the sorted list of entries in a . + /// + private struct Index : IDisposable + { + private int _count; + private int _capacity; + private KeyValue[] _entries; + private MemoryResource _memoryResource; + + /// + /// The number of elements currently in the . + /// + public int Count => _count; + + /// + /// Initializes the + /// + /// The maximum number of elements the will be able to hold. + /// The that will be used to allocate + /// memory for values added to the . + /// The of the operation. + 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; + } + } + + /// + /// Adds the specified key and value to the . + /// The existing value is replaced if the key already exists. + /// + /// The key to add. + /// The value to add. + /// The of the operation. + public Result Set(ref TKey key, ReadOnlySpan 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; + } + + /// + /// 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 with already sorted entries. + /// + /// The key to add. + /// The value to add. + /// The of the operation. + 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; + } + + /// + /// Removes all keys and values from the . + /// + public void Clear() + { + Span entries = _entries.AsSpan(0, _count); + + for (int i = 0; i < entries.Length; i++) + { + _memoryResource.Deallocate(ref entries[i].Value, Alignment); + } + + _count = 0; + } + + /// + /// Deletes an element from the . + /// + /// The key of the element to delete. + /// if the item was found and deleted. + /// if the key was not in the store. + 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; + } + + /// + /// Returns an iterator starting at the first element in the . + /// + /// The created iterator. + public Iterator GetBeginIterator() + { + return new Iterator(_entries, 0, _count); + } + + /// + /// Returns a read-only iterator starting at the first element in the . + /// + /// The created iterator. + public ConstIterator GetBeginConstIterator() + { + return new ConstIterator(_entries, 0, _count); + } + + /// + /// Returns an iterator starting at the first element greater than or equal to . + /// + /// The key at which to begin iteration. + /// The created iterator. + public Iterator GetLowerBoundIterator(ref TKey key) + { + int index = GetLowerBoundIndex(ref key); + + return new Iterator(_entries, index, _count); + } + + /// + /// Returns a read-only iterator starting at the first element greater than or equal to . + /// + /// The key at which to begin iteration. + /// The created iterator. + public ConstIterator GetLowerBoundConstIterator(ref TKey key) + { + int index = GetLowerBoundIndex(ref key); + + return new ConstIterator(_entries, index, _count); + } + + /// + /// 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. + /// + /// The iterator to fix. + /// The key that was added or removed. + 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 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; + } + } + + /// + /// Iterates through the elements in a . + /// + 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 GetValue() => _entries[_index].Value.Get(); + + public void Next() => _index++; + public bool IsEnd() => _index == _length; + + /// + /// 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. + /// + /// The index of the added or removed entry. + /// The new length of the list. + /// + 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; + } + } + } + + /// + /// Iterates through the elements in a . + /// + 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 GetValue() => _entries[_index].Value.Get(); + + public void Next() => _index++; + public bool IsEnd() => _index == _length; + } + } +} diff --git a/src/LibHac/Kvdb/KeyValueArchive.cs b/src/LibHac/Kvdb/KeyValueArchive.cs new file mode 100644 index 00000000..296ae420 --- /dev/null +++ b/src/LibHac/Kvdb/KeyValueArchive.cs @@ -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(); + } + + public void AddEntry(int keySize, int valueSize) + { + Size += Unsafe.SizeOf() + keySize + valueSize; + } + } + + internal ref struct KeyValueArchiveBufferReader + { + private ReadOnlySpan _buffer; + private int _offset; + + public KeyValueArchiveBufferReader(ReadOnlySpan 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 keyBuffer, Span 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 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 destBuffer) + { + Result rc = Peek(destBuffer); + if (rc.IsFailure()) return rc; + + _offset += destBuffer.Length; + return Result.Success; + } + } + + internal ref struct KeyValueArchiveBufferWriter + { + private Span _buffer; + private int _offset; + + public KeyValueArchiveBufferWriter(Span buffer) + { + _buffer = buffer; + _offset = 0; + } + + private void Write(ReadOnlySpan 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 key, ReadOnlySpan 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); + } + } +} diff --git a/src/LibHac/Kvdb/ResultKvdb.cs b/src/LibHac/Kvdb/ResultKvdb.cs index 3eece918..7c84bb59 100644 --- a/src/LibHac/Kvdb/ResultKvdb.cs +++ b/src/LibHac/Kvdb/ResultKvdb.cs @@ -15,8 +15,8 @@ namespace LibHac.Kvdb { public const int ModuleKvdb = 20; - /// Error code: 2020-0001; Inner value: 0x214 - public static Result.Base TooLargeKeyOrDbFull => new Result.Base(ModuleKvdb, 1); + /// There is no more space in the database or the key is too long.
Error code: 2020-0001; Inner value: 0x214
+ public static Result.Base OutOfKeyResource => new Result.Base(ModuleKvdb, 1); /// Error code: 2020-0002; Inner value: 0x414 public static Result.Base KeyNotFound => new Result.Base(ModuleKvdb, 2); /// Error code: 2020-0004; Inner value: 0x814 diff --git a/src/LibHac/LibHac.csproj b/src/LibHac/LibHac.csproj index 01958391..4e681f76 100644 --- a/src/LibHac/LibHac.csproj +++ b/src/LibHac/LibHac.csproj @@ -44,6 +44,7 @@ + diff --git a/src/LibHac/MemoryResource.cs b/src/LibHac/MemoryResource.cs new file mode 100644 index 00000000..0e8da0ea --- /dev/null +++ b/src/LibHac/MemoryResource.cs @@ -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); + + /// + /// Represents a region of memory allocated by a . + /// + public struct Buffer + { + private Memory _memory; + + /// + /// A field where implementers can store info about the . + /// + internal object Extra { get; } + + /// + /// The length of the buffer in bytes. + /// + public readonly int Length => _memory.Length; + + /// + /// Gets a span from the . + /// + public readonly Span Get() => _memory.Span; + + /// + /// Returns if the is valid. + /// + public readonly bool IsValid => !_memory.Equals(default); + + internal Buffer(Memory memory, object extra = null) + { + _memory = memory; + Extra = extra; + } + } + } + + public class ArrayPoolMemoryResource : MemoryResource + { + protected override Buffer DoAllocate(long size, int alignment) + { + byte[] array = ArrayPool.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.Shared.Return(array); + } + else + { + throw new LibHacException("Buffer was not allocated by this MemoryResource."); + } + } + + protected override bool DoIsEqual(MemoryResource other) + { + return ReferenceEquals(this, other); + } + } +} diff --git a/tests/LibHac.Tests/FullCycleRandom.cs b/tests/LibHac.Tests/FullCycleRandom.cs new file mode 100644 index 00000000..bd5866ad --- /dev/null +++ b/tests/LibHac.Tests/FullCycleRandom.cs @@ -0,0 +1,54 @@ +using System; +using System.Numerics; + +namespace LibHac.Tests +{ + /// + /// Simple, full-cycle PRNG for use in tests. + /// + 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; + } + } +} diff --git a/tests/LibHac.Tests/Kvdb/FlatMapKeyValueStoreTests.cs b/tests/LibHac.Tests/Kvdb/FlatMapKeyValueStoreTests.cs new file mode 100644 index 00000000..455d5dda --- /dev/null +++ b/tests/LibHac.Tests/Kvdb/FlatMapKeyValueStoreTests.cs @@ -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 kvStore, FileSystemClient fsClient) Create(int capacity) + where T : unmanaged, IEquatable, IComparable + { + FileSystemClient fsClient = FileSystemServerFactory.CreateClient(false); + + var mountedFs = new InMemoryFileSystem(); + fsClient.Register(MountName, mountedFs).ThrowIfFailure(); + + FlatMapKeyValueStore kvStore = Create(fsClient, capacity); + + return (kvStore, fsClient); + } + + private static FlatMapKeyValueStore Create(FileSystemClient fsClient, int capacity) + where T : unmanaged, IEquatable, IComparable + { + var memoryResource = new ArrayPoolMemoryResource(); + + var kvStore = new FlatMapKeyValueStore(); + 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 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 kvStore, FileSystemClient _) = Create(10); + + Assert.Equal(0, kvStore.Count); + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(10)] + public void Count_PopulatedStore_ReturnsCorrectCount(int count) + { + (FlatMapKeyValueStore kvStore, FileSystemClient _) = Create(10); + Assert.Success(PopulateKvStore(kvStore, out _, count)); + + Assert.Equal(count, kvStore.Count); + } + + [Fact] + public void Load_FileDoesNotExist_ExistingEntriesAreCleared() + { + const int count = 10; + + (FlatMapKeyValueStore kvStore, FileSystemClient _) = Create(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 kvStore, FileSystemClient fsClient) = Create(count + 5); + Assert.Success(PopulateKvStore(kvStore, out byte[][] values, count)); + + Assert.Success(kvStore.Save()); + kvStore.Dispose(); + + kvStore = Create(fsClient, count + 5); + Assert.Success(kvStore.Load()); + + FlatMapKeyValueStore.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.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 kvStore, FileSystemClient fsClient) = Create(count); + Assert.Success(PopulateKvStore(kvStore, out _, count)); + + Assert.Success(kvStore.Save()); + kvStore.Dispose(); + + + kvStore = Create(fsClient, count - 5); + Assert.Result(ResultKvdb.OutOfKeyResource, kvStore.Load()); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(10)] + public void Save_ArchiveFileIsWrittenToDisk(int count) + { + (FlatMapKeyValueStore kvStore, FileSystemClient fsClient) = Create(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 kvStore, FileSystemClient _) = Create(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 kvStore, FileSystemClient _) = Create(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 kvStore, FileSystemClient _) = Create(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 kvStore, FileSystemClient _) = Create(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 kvStore, FileSystemClient _) = Create(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 kvStore, FileSystemClient _) = Create(entryCount + 10); + Assert.Success(PopulateKvStore(kvStore, out byte[][] values, entryCount, 20, rngSeed)); + + FlatMapKeyValueStore.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.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 kvStore, FileSystemClient _) = Create(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 kvStore, FileSystemClient _) = Create(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 kvStore, FileSystemClient _) = Create(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 kvStore, FileSystemClient _) = Create(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 kvStore, FileSystemClient _) = Create(count); + Assert.Success(PopulateKvStore(kvStore, out _, count)); + + TTest keyToDelete = entryToDelete; + Assert.Success(kvStore.Delete(ref keyToDelete)); + + FlatMapKeyValueStore.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 kvStore, FileSystemClient _) = Create(count); + Assert.Success(PopulateKvStore(kvStore, out _, count)); + + TTest startingKey = startEntry; + FlatMapKeyValueStore.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 kvStore, FileSystemClient _) = Create(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.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 kvStore, FileSystemClient _) = Create(count); + Assert.Success(PopulateKvStore(kvStore, out _, count)); + + TTest key = startIndex; + FlatMapKeyValueStore.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 kvStore, FileSystemClient _) = Create(count); + Assert.Success(PopulateKvStore(kvStore, out _, count)); + + FlatMapKeyValueStore.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 kvStore, FileSystemClient _) = Create(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.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 kvStore, FileSystemClient _) = Create(count); + + FlatMapKeyValueStore.Iterator iterator = kvStore.GetBeginIterator(); + Assert.True(iterator.IsEnd()); + } + + [Fact] + public void IteratorIsEnd_PopulatedStore_ReturnsFalseUntilFinishedIterating() + { + const int count = 10; + + (FlatMapKeyValueStore kvStore, FileSystemClient _) = Create(count); + Assert.Success(PopulateKvStore(kvStore, out _, count)); + + FlatMapKeyValueStore.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 kvStore, FileSystemClient _) = Create(count); + Assert.Success(PopulateKvStore(kvStore, out byte[][] values, count)); + + FlatMapKeyValueStore.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.KeyValue kv = ref iterator.Get(); + + Assert.Equal(expectedKey, kv.Key); + Assert.Equal(expectedValue, kv.Value.Get().ToArray()); + + iterator.Next(); + } + } + } +}