aboutsummaryrefslogtreecommitdiff
path: root/lib/Utils/src
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-01-08 16:01:54 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-01-08 16:01:54 -0500
commitde94d788e9a47432a7630a8215896b8dd3628599 (patch)
tree666dec06eef861d101cb6948aff52a3d354c8d73 /lib/Utils/src
parentbe6dc557a3b819248b014992eb96c1cb21f8112b (diff)
Reorder + analyzer cleanup
Diffstat (limited to 'lib/Utils/src')
-rw-r--r--lib/Utils/src/Async/AccessSerializer.cs297
-rw-r--r--lib/Utils/src/Async/AsyncExclusiveResource.cs169
-rw-r--r--lib/Utils/src/Async/AsyncQueue.cs144
-rw-r--r--lib/Utils/src/Async/AsyncUpdatableResource.cs111
-rw-r--r--lib/Utils/src/Async/Exceptions/AsyncUpdateException.cs52
-rw-r--r--lib/Utils/src/Async/IAsyncExclusiveResource.cs40
-rw-r--r--lib/Utils/src/Async/IAsyncWaitHandle.cs41
-rw-r--r--lib/Utils/src/Async/IWaitHandle.cs59
-rw-r--r--lib/Utils/src/BitField.cs115
-rw-r--r--lib/Utils/src/ERRNO.cs152
-rw-r--r--lib/Utils/src/Extensions/CacheExtensions.cs407
-rw-r--r--lib/Utils/src/Extensions/CollectionExtensions.cs100
-rw-r--r--lib/Utils/src/Extensions/IoExtensions.cs401
-rw-r--r--lib/Utils/src/Extensions/JsonExtensions.cs215
-rw-r--r--lib/Utils/src/Extensions/MemoryExtensions.cs769
-rw-r--r--lib/Utils/src/Extensions/MutexReleaser.cs62
-rw-r--r--lib/Utils/src/Extensions/SafeLibraryExtensions.cs103
-rw-r--r--lib/Utils/src/Extensions/SemSlimReleaser.cs62
-rw-r--r--lib/Utils/src/Extensions/StringExtensions.cs481
-rw-r--r--lib/Utils/src/Extensions/ThreadingExtensions.cs226
-rw-r--r--lib/Utils/src/Extensions/TimerExtensions.cs71
-rw-r--r--lib/Utils/src/Extensions/TimerResetHandle.cs66
-rw-r--r--lib/Utils/src/Extensions/VnStringExtensions.cs418
-rw-r--r--lib/Utils/src/IIndexable.cs43
-rw-r--r--lib/Utils/src/IO/ArrayPoolStreamBuffer.cs70
-rw-r--r--lib/Utils/src/IO/BackingStream.cs181
-rw-r--r--lib/Utils/src/IO/FileOperations.cs105
-rw-r--r--lib/Utils/src/IO/IDataAccumulator.cs64
-rw-r--r--lib/Utils/src/IO/ISlindingWindowBuffer.cs91
-rw-r--r--lib/Utils/src/IO/IVnTextReader.cs72
-rw-r--r--lib/Utils/src/IO/InMemoryTemplate.cs196
-rw-r--r--lib/Utils/src/IO/IsolatedStorageDirectory.cs154
-rw-r--r--lib/Utils/src/IO/SlidingWindowBufferExtensions.cs213
-rw-r--r--lib/Utils/src/IO/TemporayIsolatedFile.cs57
-rw-r--r--lib/Utils/src/IO/VnMemoryStream.cs469
-rw-r--r--lib/Utils/src/IO/VnStreamReader.cs180
-rw-r--r--lib/Utils/src/IO/VnStreamWriter.cs292
-rw-r--r--lib/Utils/src/IO/VnTextReaderExtensions.cs223
-rw-r--r--lib/Utils/src/IO/WriteOnlyBufferedStream.cs255
-rw-r--r--lib/Utils/src/IObjectStorage.cs48
-rw-r--r--lib/Utils/src/Logging/ILogProvider.cs79
-rw-r--r--lib/Utils/src/Logging/LogLevel.cs33
-rw-r--r--lib/Utils/src/Logging/LoggerExtensions.cs60
-rw-r--r--lib/Utils/src/Memory/Caching/ICacheHolder.cs45
-rw-r--r--lib/Utils/src/Memory/Caching/ICacheable.cs44
-rw-r--r--lib/Utils/src/Memory/Caching/IObjectRental.cs47
-rw-r--r--lib/Utils/src/Memory/Caching/IReusable.cs42
-rw-r--r--lib/Utils/src/Memory/Caching/LRUCache.cs127
-rw-r--r--lib/Utils/src/Memory/Caching/LRUDataStore.cs232
-rw-r--r--lib/Utils/src/Memory/Caching/ObjectRental.cs236
-rw-r--r--lib/Utils/src/Memory/Caching/ObjectRentalBase.cs155
-rw-r--r--lib/Utils/src/Memory/Caching/ReusableStore.cs61
-rw-r--r--lib/Utils/src/Memory/Caching/ThreadLocalObjectStorage.cs76
-rw-r--r--lib/Utils/src/Memory/Caching/ThreadLocalReusableStore.cs64
-rw-r--r--lib/Utils/src/Memory/ForwardOnlyBufferWriter.cs122
-rw-r--r--lib/Utils/src/Memory/ForwardOnlyMemoryReader.cs74
-rw-r--r--lib/Utils/src/Memory/ForwardOnlyMemoryWriter.cs122
-rw-r--r--lib/Utils/src/Memory/ForwardOnlyReader.cs74
-rw-r--r--lib/Utils/src/Memory/IMemoryHandle.cs53
-rw-r--r--lib/Utils/src/Memory/IStringSerializeable.cs55
-rw-r--r--lib/Utils/src/Memory/IUnmangedHeap.cs59
-rw-r--r--lib/Utils/src/Memory/Memory.cs456
-rw-r--r--lib/Utils/src/Memory/MemoryHandle.cs237
-rw-r--r--lib/Utils/src/Memory/PrivateBuffersMemoryPool.cs67
-rw-r--r--lib/Utils/src/Memory/PrivateHeap.cs184
-rw-r--r--lib/Utils/src/Memory/PrivateString.cs183
-rw-r--r--lib/Utils/src/Memory/PrivateStringManager.cs117
-rw-r--r--lib/Utils/src/Memory/ProcessHeap.cs82
-rw-r--r--lib/Utils/src/Memory/RpMallocPrivateHeap.cs279
-rw-r--r--lib/Utils/src/Memory/SubSequence.cs113
-rw-r--r--lib/Utils/src/Memory/SysBufferMemoryManager.cs102
-rw-r--r--lib/Utils/src/Memory/UnmanagedHeapBase.cs185
-rw-r--r--lib/Utils/src/Memory/UnsafeMemoryHandle.cs231
-rw-r--r--lib/Utils/src/Memory/VnString.cs497
-rw-r--r--lib/Utils/src/Memory/VnTable.cs213
-rw-r--r--lib/Utils/src/Memory/VnTempBuffer.cs207
-rw-r--r--lib/Utils/src/Native/SafeLibraryHandle.cs220
-rw-r--r--lib/Utils/src/Native/SafeMethodHandle.cs61
-rw-r--r--lib/Utils/src/NativeLibraryException.cs89
-rw-r--r--lib/Utils/src/Resources/BackedResourceBase.cs79
-rw-r--r--lib/Utils/src/Resources/CallbackOpenHandle.cs44
-rw-r--r--lib/Utils/src/Resources/ExclusiveResourceHandle.cs81
-rw-r--r--lib/Utils/src/Resources/IExclusiveResource.cs39
-rw-r--r--lib/Utils/src/Resources/IResource.cs38
-rw-r--r--lib/Utils/src/Resources/OpenHandle.cs38
-rw-r--r--lib/Utils/src/Resources/OpenResourceHandle.cs44
-rw-r--r--lib/Utils/src/Resources/ResourceDeleteFailedException.cs40
-rw-r--r--lib/Utils/src/Resources/ResourceUpdateFailedException.cs40
-rw-r--r--lib/Utils/src/Resources/UpdatableResource.cs113
-rw-r--r--lib/Utils/src/VNLib.Utils.csproj47
-rw-r--r--lib/Utils/src/VnDisposeable.cs85
-rw-r--r--lib/Utils/src/VnEncoding.cs914
92 files changed, 14259 insertions, 0 deletions
diff --git a/lib/Utils/src/Async/AccessSerializer.cs b/lib/Utils/src/Async/AccessSerializer.cs
new file mode 100644
index 0000000..ce78f6c
--- /dev/null
+++ b/lib/Utils/src/Async/AccessSerializer.cs
@@ -0,0 +1,297 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: AccessSerializer.cs
+*
+* AccessSerializer.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Resources;
+
+namespace VNLib.Utils.Async
+{
+ /// <summary>
+ /// Provides access arbitration to an exclusive resouce
+ /// </summary>
+ /// <typeparam name="TKey">The uinique identifier type for the resource</typeparam>
+ /// <typeparam name="TResource">The resource type</typeparam>
+ public sealed class AccessSerializer<TKey, TResource> where TResource : IExclusiveResource
+ {
+ private readonly SemaphoreSlim semaphore;
+ private readonly Func<TKey, TResource> Factory;
+ private readonly Action CompletedCb;
+ private int WaitingCount;
+ /// <summary>
+ /// Creates a new <see cref="AccessSerializer{K, T}"/> with the specified factory and completed callback
+ /// </summary>
+ /// <param name="factory">Factory function to genereate new <typeparamref name="TResource"/> objects from <typeparamref name="TKey"/> keys</param>
+ /// <param name="completedCb">Function to be invoked when the encapsulated objected is no longer in use</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ public AccessSerializer(Func<TKey, TResource> factory, Action completedCb)
+ {
+ this.Factory = factory ?? throw new ArgumentNullException(nameof(factory));
+ this.CompletedCb = completedCb;
+ //Setup semaphore for locking
+ this.semaphore = new SemaphoreSlim(1, 1);
+ this.WaitingCount = 0;
+ }
+
+ /// <summary>
+ /// Attempts to obtain an exclusive lock on the object
+ /// </summary>
+ /// <param name="key"></param>
+ /// <param name="wait">Time to wait for lock</param>
+ /// <param name="exObj"></param>
+ /// <returns>true if lock was obtained within the timeout, false if the lock was not obtained</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public bool TryWait(TKey key, TimeSpan wait, out ExclusiveResourceHandle<TResource> exObj)
+ {
+ //Increase waiting count while we wait
+ Interlocked.Increment(ref WaitingCount);
+ try
+ {
+ //Try to obtain the lock
+ if (semaphore.Wait(wait))
+ {
+ TResource get() => Factory(key);
+ //Create new exclusive lock handle that will generate a new that calls release when freed
+ exObj = new(get, Release);
+ return true;
+ }
+ //Lock not taken
+ exObj = null;
+ return false;
+ }
+ finally
+ {
+ //Decrease the waiting count since we are no longer waiting
+ Interlocked.Decrement(ref WaitingCount);
+ }
+ }
+ /// <summary>
+ /// Waits for exclusive access to the resource.
+ /// </summary>
+ /// <param name="key"></param>
+ /// <returns>An <see cref="ExclusiveResourceHandle{T}"/> encapsulating the resource</returns>
+ public ExclusiveResourceHandle<TResource> Wait(TKey key)
+ {
+ try
+ {
+ //Increase waiting count while we wait
+ Interlocked.Increment(ref WaitingCount);
+ //Try to obtain the lock
+ semaphore.Wait();
+ //Local function to generate the output value
+ TResource get() => Factory(key);
+ //Create new exclusive lock handle that will generate a new that calls release when freed
+ return new(get, Release);
+ }
+ finally
+ {
+ //Decrease the waiting count since we are no longer waiting
+ Interlocked.Decrement(ref WaitingCount);
+ }
+ }
+ /// <summary>
+ /// Asynchronously waits for exclusive access to the resource.
+ /// </summary>
+ /// <returns>An <see cref="ExclusiveResourceHandle{TResource}"/> encapsulating the resource</returns>
+ public async Task<ExclusiveResourceHandle<TResource>> WaitAsync(TKey key, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ //Increase waiting count while we wait
+ Interlocked.Increment(ref WaitingCount);
+ //Try to obtain the lock
+ await semaphore.WaitAsync(cancellationToken);
+ //Local function to generate the output value
+ TResource get() => Factory(key);
+ //Create new exclusive lock handle that will generate a new that calls release when freed
+ return new(get, Release);
+ }
+ finally
+ {
+ //Decrease the waiting count since we are no longer waiting
+ Interlocked.Decrement(ref WaitingCount);
+ }
+ }
+ /// <summary>
+ /// Releases an exclusive lock that is held on an object
+ /// </summary>
+ private void Release()
+ {
+ /*
+ * If objects are waiting for the current instance, then we will release
+ * the semaphore and exit, as we no longer have control over the context
+ */
+ if (WaitingCount > 0)
+ {
+ this.semaphore.Release();
+ }
+ else
+ {
+ //Do not release the sempahore, just dispose of the semaphore
+ this.semaphore.Dispose();
+ //call the completed function
+ CompletedCb?.Invoke();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Provides access arbitration to an <see cref="IExclusiveResource"/>
+ /// </summary>
+ /// <typeparam name="TKey">The uinique identifier type for the resource</typeparam>
+ /// <typeparam name="TArg">The type of the optional argument to be passed to the user-implemented factory function</typeparam>
+ /// <typeparam name="TResource">The resource type</typeparam>
+ public sealed class AccessSerializer<TKey, TArg, TResource> where TResource : IExclusiveResource
+ {
+ private readonly SemaphoreSlim semaphore;
+ private readonly Func<TKey, TArg, TResource> Factory;
+ private readonly Action CompletedCb;
+ private int WaitingCount;
+ /// <summary>
+ /// Creates a new <see cref="AccessSerializer{TKey, TArg, TResource}"/> with the specified factory and completed callback
+ /// </summary>
+ /// <param name="factory">Factory function to genereate new <typeparamref name="TResource"/> objects from <typeparamref name="TKey"/> keys</param>
+ /// <param name="completedCb">Function to be invoked when the encapsulated objected is no longer in use</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ public AccessSerializer(Func<TKey, TArg, TResource> factory, Action completedCb)
+ {
+ this.Factory = factory ?? throw new ArgumentNullException(nameof(factory));
+ this.CompletedCb = completedCb;
+ //Setup semaphore for locking
+ this.semaphore = new SemaphoreSlim(1, 1);
+ this.WaitingCount = 0;
+ }
+
+ /// <summary>
+ /// Attempts to obtain an exclusive lock on the object
+ /// </summary>
+ /// <param name="key"></param>
+ /// <param name="arg">The key identifying the resource</param>
+ /// <param name="wait">Time to wait for lock</param>
+ /// <param name="exObj"></param>
+ /// <returns>true if lock was obtained within the timeout, false if the lock was not obtained</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public bool TryWait(TKey key, TArg arg, TimeSpan wait, out ExclusiveResourceHandle<TResource> exObj)
+ {
+ //Increase waiting count while we wait
+ Interlocked.Increment(ref WaitingCount);
+ try
+ {
+ //Try to obtain the lock
+ if (semaphore.Wait(wait))
+ {
+ TResource get() => Factory(key, arg);
+ //Create new exclusive lock handle that will generate a new that calls release when freed
+ exObj = new(get, Release);
+ return true;
+ }
+ //Lock not taken
+ exObj = null;
+ return false;
+ }
+ finally
+ {
+ //Decrease the waiting count since we are no longer waiting
+ Interlocked.Decrement(ref WaitingCount);
+ }
+ }
+ /// <summary>
+ /// Waits for exclusive access to the resource.
+ /// </summary>
+ /// <param name="key">The unique key that identifies the resource</param>
+ /// <param name="arg">The state argument to pass to the factory function</param>
+ /// <returns>An <see cref="ExclusiveResourceHandle{TResource}"/> encapsulating the resource</returns>
+ public ExclusiveResourceHandle<TResource> Wait(TKey key, TArg arg)
+ {
+ try
+ {
+ //Increase waiting count while we wait
+ Interlocked.Increment(ref WaitingCount);
+ //Try to obtain the lock
+ semaphore.Wait();
+ //Local function to generate the output value
+ TResource get() => Factory(key, arg);
+ //Create new exclusive lock handle that will generate a new that calls release when freed
+ return new(get, Release);
+ }
+ finally
+ {
+ //Decrease the waiting count since we are no longer waiting
+ Interlocked.Decrement(ref WaitingCount);
+ }
+ }
+ /// <summary>
+ /// Asynchronously waits for exclusive access to the resource.
+ /// </summary>
+ /// <param name="key"></param>
+ /// <param name="arg">The state argument to pass to the factory function</param>
+ /// <param name="cancellationToken"></param>
+ /// <returns>An <see cref="ExclusiveResourceHandle{TResource}"/> encapsulating the resource</returns>
+ public async Task<ExclusiveResourceHandle<TResource>> WaitAsync(TKey key, TArg arg, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ //Increase waiting count while we wait
+ Interlocked.Increment(ref WaitingCount);
+ //Try to obtain the lock
+ await semaphore.WaitAsync(cancellationToken);
+ //Local function to generate the output value
+ TResource get() => Factory(key, arg);
+ //Create new exclusive lock handle that will generate a new that calls release when freed
+ return new(get, Release);
+ }
+ finally
+ {
+ //Decrease the waiting count since we are no longer waiting
+ Interlocked.Decrement(ref WaitingCount);
+ }
+ }
+
+ /// <summary>
+ /// Releases an exclusive lock that is held on an object
+ /// </summary>
+ private void Release()
+ {
+ /*
+ * If objects are waiting for the current instance, then we will release
+ * the semaphore and exit, as we no longer have control over the context
+ */
+ if (WaitingCount > 0)
+ {
+ this.semaphore.Release();
+ }
+ else
+ {
+ //Do not release the sempahore, just dispose of the semaphore
+ this.semaphore.Dispose();
+ //call the completed function
+ CompletedCb?.Invoke();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Async/AsyncExclusiveResource.cs b/lib/Utils/src/Async/AsyncExclusiveResource.cs
new file mode 100644
index 0000000..18e2a42
--- /dev/null
+++ b/lib/Utils/src/Async/AsyncExclusiveResource.cs
@@ -0,0 +1,169 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: AsyncExclusiveResource.cs
+*
+* AsyncExclusiveResource.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace VNLib.Utils.Async
+{
+ /// <summary>
+ /// Provides a base class for resources that must be obtained exclusivly in a multi-threaded environment
+ /// but allow state update operations (and their exceptions) to be deferred to the next accessor.
+ /// </summary>
+ /// <typeparam name="TState">The state parameter type passed during updates</typeparam>
+ public abstract class AsyncExclusiveResource<TState> : VnDisposeable, IWaitHandle, IAsyncWaitHandle
+ {
+ /// <summary>
+ /// Main mutli-threading lock used for primary access synchronization
+ /// </summary>
+ protected SemaphoreSlim MainLock { get; } = new (1, 1);
+
+ private Task? LastUpdate;
+
+ /// <summary>
+ /// <inheritdoc/>
+ /// <br></br>
+ /// <br></br>
+ /// If the previous call to <see cref="UpdateAndRelease"/> resulted in an asynchronous update, and exceptions occured, an <see cref="AsyncUpdateException"/>
+ /// will be thrown enclosing the exception
+ /// </summary>
+ /// <param name="millisecondsTimeout">Time in milliseconds to wait for exclusive access to the resource</param>
+ /// <exception cref="AsyncUpdateException"></exception>
+ /// <inheritdoc/>
+ public virtual bool WaitOne(int millisecondsTimeout)
+ {
+ //First wait for main lock
+ if (MainLock.Wait(millisecondsTimeout))
+ {
+ //Main lock has been taken
+ try
+ {
+ //Wait for async update if there is one pending(will throw exceptions if any occurred)
+ LastUpdate?.Wait();
+ return true;
+ }
+ catch (AggregateException ae) when (ae.InnerException != null)
+ {
+ //Release the main lock and re-throw the inner exception
+ _ = MainLock.Release();
+ //Throw a new async update exception
+ throw new AsyncUpdateException(ae.InnerException);
+ }
+ catch
+ {
+ //Release the main lock and re-throw the exception
+ _ = MainLock.Release();
+ throw;
+ }
+ }
+ return false;
+ }
+
+ ///<inheritdoc/>
+ ///<exception cref="ObjectDisposedException"></exception>
+ public virtual async Task WaitOneAsync(CancellationToken token = default)
+ {
+ //Wait for main lock
+ await MainLock.WaitAsync(token).ConfigureAwait(true);
+ //if the last update completed synchronously, return true
+ if (LastUpdate == null)
+ {
+ return;
+ }
+ try
+ {
+ //Await the last update task and catch its exceptions
+ await LastUpdate.ConfigureAwait(false);
+ }
+ catch
+ {
+ //Release the main lock and re-throw the exception
+ _ = MainLock.Release();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Requests a resource update and releases the exclusive lock on this resource. If a deferred update operation has any
+ /// exceptions during its last operation, they will be thrown here.
+ /// </summary>
+ /// <param name="defer">Specifies weather the update should be deferred or awaited on the current call</param>
+ /// <param name="state">A state parameter to be pased to the update function</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public async ValueTask UpdateAndRelease(bool defer, TState state)
+ {
+ //Otherwise wait and update on the current thread
+ try
+ {
+ //Dispose the update task
+ LastUpdate?.Dispose();
+ //Remove the referrence
+ LastUpdate = null;
+ //Run update on the current thread
+ LastUpdate = await UpdateResource(defer, state).ConfigureAwait(true);
+ //If the update is not deferred, await the results
+ if (!defer && LastUpdate != null)
+ {
+ await LastUpdate.ConfigureAwait(true);
+ }
+ }
+ finally
+ {
+ //Release the main lock
+ _ = MainLock.Release();
+ }
+ }
+
+ /// <summary>
+ /// <para>
+ /// When overrriden in a derived class, is responsible for updating the state of the instance if necessary.
+ /// </para>
+ /// <para>
+ /// If the result of the update retruns a <see cref="Task"/> that represents the deferred update, the next call to <see cref="WaitOne"/> will
+ /// block until the operation completes and will throw any exceptions that occured
+ /// </para>
+ /// </summary>
+ /// <param name="defer">true if the caller expects a resource update to be deferred, false if the caller expects the result of the update to be awaited</param>
+ /// <param name="state">State parameter passed when releasing</param>
+ /// <returns>A <see cref="Task"/> representing the async state update operation, or null if no async state update operation need's to be monitored</returns>
+ protected abstract ValueTask<Task?> UpdateResource(bool defer, TState state);
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ //Dispose lock
+ MainLock.Dispose();
+
+ //Try to cleanup the last update
+ if (LastUpdate != null && LastUpdate.IsCompletedSuccessfully)
+ {
+ LastUpdate.Dispose();
+ }
+
+ LastUpdate = null;
+ }
+
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Async/AsyncQueue.cs b/lib/Utils/src/Async/AsyncQueue.cs
new file mode 100644
index 0000000..ba45513
--- /dev/null
+++ b/lib/Utils/src/Async/AsyncQueue.cs
@@ -0,0 +1,144 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: AsyncQueue.cs
+*
+* AsyncQueue.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Threading.Channels;
+using System.Diagnostics.CodeAnalysis;
+
+namespace VNLib.Utils.Async
+{
+ /// <summary>
+ /// Provides a <see cref="Channel{T}"/> based asynchronous queue
+ /// </summary>
+ /// <typeparam name="T">The event object type</typeparam>
+ public class AsyncQueue<T>
+ {
+ private readonly Channel<T> _channel;
+
+ /// <summary>
+ /// Initalizes a new multi-threaded bound channel queue, that accepts
+ /// the <paramref name="capacity"/> number of items before it will
+ /// return asynchronously, or fail to enqueue items
+ /// </summary>
+ /// <param name="capacity">The maxium number of items to allow in the queue</param>
+ public AsyncQueue(int capacity):this(false, false, capacity)
+ {}
+ /// <summary>
+ /// Initalizes a new multi-threaded unbound channel queue
+ /// </summary>
+ public AsyncQueue():this(false, false)
+ {}
+
+ /// <summary>
+ /// Initalizes a new queue that allows specifying concurrency requirements
+ /// and a bound/unbound channel capacity
+ /// </summary>
+ /// <param name="singleWriter">A value that specifies only a single thread be enqueing items?</param>
+ /// <param name="singleReader">A value that specifies only a single thread will be dequeing</param>
+ /// <param name="capacity">The maxium number of items to enque without failing</param>
+ public AsyncQueue(bool singleWriter, bool singleReader, int capacity = int.MaxValue)
+ {
+ if(capacity == int.MaxValue)
+ {
+ //Create unbounded
+ UnboundedChannelOptions opt = new()
+ {
+ SingleReader = singleReader,
+ SingleWriter = singleWriter,
+ AllowSynchronousContinuations = true,
+ };
+ _channel = Channel.CreateUnbounded<T>(opt);
+ }
+ else
+ {
+ //Create bounded
+ BoundedChannelOptions opt = new(capacity)
+ {
+ SingleReader = singleReader,
+ SingleWriter = singleWriter,
+ AllowSynchronousContinuations = true,
+ //Default wait for space
+ FullMode = BoundedChannelFullMode.Wait
+ };
+ _channel = Channel.CreateBounded<T>(opt);
+ }
+ }
+
+ /// <summary>
+ /// Initalizes a new unbound channel based queue
+ /// </summary>
+ /// <param name="ubOptions">Channel options</param>
+ public AsyncQueue(UnboundedChannelOptions ubOptions)
+ {
+ _channel = Channel.CreateUnbounded<T>(ubOptions);
+ }
+
+ /// <summary>
+ /// Initalizes a new bound channel based queue
+ /// </summary>
+ /// <param name="options">Channel options</param>
+ public AsyncQueue(BoundedChannelOptions options)
+ {
+ _channel = Channel.CreateBounded<T>(options);
+ }
+
+ /// <summary>
+ /// Attemts to enqeue an item if the queue has the capacity
+ /// </summary>
+ /// <param name="item">The item to eqneue</param>
+ /// <returns>True if the queue can accept another item, false otherwise</returns>
+ public bool TryEnque(T item) => _channel.Writer.TryWrite(item);
+ /// <summary>
+ /// Enqueues an item to the end of the queue and notifies a waiter that an item was enqueued
+ /// </summary>
+ /// <param name="item">The item to enqueue</param>
+ /// <param name="cancellationToken"></param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public ValueTask EnqueueAsync(T item, CancellationToken cancellationToken = default) => _channel.Writer.WriteAsync(item, cancellationToken);
+ /// <summary>
+ /// Asynchronously waits for an item to be Enqueued to the end of the queue.
+ /// </summary>
+ /// <returns>The item at the begining of the queue</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public ValueTask<T> DequeueAsync(CancellationToken cancellationToken = default) => _channel.Reader.ReadAsync(cancellationToken);
+ /// <summary>
+ /// Removes the object at the beginning of the queue and stores it to the result parameter. Without waiting for a change
+ /// event.
+ /// </summary>
+ /// <param name="result">The item that was at the begining of the queue</param>
+ /// <returns>True if the queue could be read synchronously, false if the lock could not be entered, or the queue contains no items</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public bool TryDequeue([MaybeNullWhen(false)] out T result) => _channel.Reader.TryRead(out result);
+ /// <summary>
+ /// Peeks the object at the beginning of the queue and stores it to the result parameter. Without waiting for a change
+ /// event.
+ /// </summary>
+ /// <param name="result">The item that was at the begining of the queue</param>
+ /// <returns>True if the queue could be read synchronously, false if the lock could not be entered, or the queue contains no items</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public bool TryPeek([MaybeNullWhen(false)] out T result) => _channel.Reader.TryPeek(out result);
+ }
+}
diff --git a/lib/Utils/src/Async/AsyncUpdatableResource.cs b/lib/Utils/src/Async/AsyncUpdatableResource.cs
new file mode 100644
index 0000000..b4ce519
--- /dev/null
+++ b/lib/Utils/src/Async/AsyncUpdatableResource.cs
@@ -0,0 +1,111 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: AsyncUpdatableResource.cs
+*
+* AsyncUpdatableResource.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+using VNLib.Utils.IO;
+using VNLib.Utils.Resources;
+
+namespace VNLib.Utils.Async
+{
+ /// <summary>
+ /// A callback delegate used for updating a <see cref="AsyncUpdatableResource"/>
+ /// </summary>
+ /// <param name="source">The <see cref="AsyncUpdatableResource"/> to be updated</param>
+ /// <param name="data">The serialized data to be stored/updated</param>
+ /// <exception cref="ResourceUpdateFailedException"></exception>
+ public delegate Task AsyncUpdateCallback(object source, Stream data);
+ /// <summary>
+ /// A callback delegate invoked when a <see cref="AsyncUpdatableResource"/> delete is requested
+ /// </summary>
+ /// <param name="source">The <see cref="AsyncUpdatableResource"/> to be deleted</param>
+ /// <exception cref="ResourceDeleteFailedException"></exception>
+ public delegate Task AsyncDeleteCallback(object source);
+
+ /// <summary>
+ /// Implemented by a resource that is backed by an external data store, that when modified or deleted will
+ /// be reflected to the backing store.
+ /// </summary>
+ public abstract class AsyncUpdatableResource : BackedResourceBase, IAsyncExclusiveResource
+ {
+ protected abstract AsyncUpdateCallback UpdateCb { get; }
+ protected abstract AsyncDeleteCallback DeleteCb { get; }
+
+ /// <summary>
+ /// Releases the resource and flushes pending changes to its backing store.
+ /// </summary>
+ /// <returns>A task that represents the async operation</returns>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ResourceDeleteFailedException"></exception>
+ /// <exception cref="ResourceUpdateFailedException"></exception>
+ public virtual async ValueTask ReleaseAsync()
+ {
+ //If resource has already been realeased, return
+ if (IsReleased)
+ {
+ return;
+ }
+ //If deleted flag is set, invoke the delete callback
+ if (Deleted)
+ {
+ await DeleteCb(this).ConfigureAwait(true);
+ }
+ //If the state has been modifed, flush changes to the store
+ else if (Modified)
+ {
+ await FlushPendingChangesAsync().ConfigureAwait(true);
+ }
+ //Set the released value
+ IsReleased = true;
+ }
+
+ /// <summary>
+ /// <para>
+ /// Writes the current state of the the resource to the backing store
+ /// immediatly by invoking the specified callback.
+ /// </para>
+ /// <para>
+ /// Only call this method if your store supports multiple state updates
+ /// </para>
+ /// </summary>
+ protected virtual async Task FlushPendingChangesAsync()
+ {
+ //Get the resource
+ object resource = GetResource();
+ //Open a memory stream to store data in
+ using VnMemoryStream data = new();
+ //Serialize and write to stream
+ VnEncoding.JSONSerializeToBinary(resource, data, resource.GetType(), base.JSO);
+ //Reset stream to begining
+ _ = data.Seek(0, SeekOrigin.Begin);
+ //Invoke update callback
+ await UpdateCb(this, data).ConfigureAwait(true);
+ //Clear modified flag
+ Modified = false;
+ }
+ }
+}
diff --git a/lib/Utils/src/Async/Exceptions/AsyncUpdateException.cs b/lib/Utils/src/Async/Exceptions/AsyncUpdateException.cs
new file mode 100644
index 0000000..de5a491
--- /dev/null
+++ b/lib/Utils/src/Async/Exceptions/AsyncUpdateException.cs
@@ -0,0 +1,52 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: AsyncUpdateException.cs
+*
+* AsyncUpdateException.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using VNLib.Utils.Resources;
+
+namespace VNLib.Utils.Async
+{
+ /// <summary>
+ /// Represents an exception that was raised during an asyncronous update of a resource. The <see cref="Exception.InnerException"/> stores the
+ /// details of the actual exception raised
+ /// </summary>
+ public sealed class AsyncUpdateException : ResourceUpdateFailedException
+ {
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="inner"></param>
+ public AsyncUpdateException(Exception inner) : base("", inner) { }
+
+ public AsyncUpdateException()
+ {}
+
+ public AsyncUpdateException(string message) : base(message)
+ {}
+
+ public AsyncUpdateException(string message, Exception innerException) : base(message, innerException)
+ {}
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Async/IAsyncExclusiveResource.cs b/lib/Utils/src/Async/IAsyncExclusiveResource.cs
new file mode 100644
index 0000000..93157ce
--- /dev/null
+++ b/lib/Utils/src/Async/IAsyncExclusiveResource.cs
@@ -0,0 +1,40 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IAsyncExclusiveResource.cs
+*
+* IAsyncExclusiveResource.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Threading.Tasks;
+using VNLib.Utils.Resources;
+
+namespace VNLib.Utils.Async
+{
+ /// <summary>
+ /// <inheritdoc/>
+ /// </summary>
+ public interface IAsyncExclusiveResource : IResource
+ {
+ /// <summary>
+ /// Releases the resource from use. Called when a <see cref="ExclusiveResourceHandle{T}"/> is disposed
+ /// </summary>
+ ValueTask ReleaseAsync();
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Async/IAsyncWaitHandle.cs b/lib/Utils/src/Async/IAsyncWaitHandle.cs
new file mode 100644
index 0000000..1cadc06
--- /dev/null
+++ b/lib/Utils/src/Async/IAsyncWaitHandle.cs
@@ -0,0 +1,41 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IAsyncWaitHandle.cs
+*
+* IAsyncWaitHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace VNLib.Utils.Async
+{
+ /// <summary>
+ /// Provides a synchronization handle that can be asynchronously aquired
+ /// </summary>
+ public interface IAsyncWaitHandle
+ {
+ /// <summary>
+ /// Waits for exclusive access to the resource until the <see cref="CancellationToken"/> expires
+ /// </summary>
+ /// <inheritdoc/>
+ Task WaitOneAsync(CancellationToken token = default);
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Async/IWaitHandle.cs b/lib/Utils/src/Async/IWaitHandle.cs
new file mode 100644
index 0000000..85e8a2a
--- /dev/null
+++ b/lib/Utils/src/Async/IWaitHandle.cs
@@ -0,0 +1,59 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IWaitHandle.cs
+*
+* IWaitHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+
+namespace VNLib.Utils.Async
+{
+ /// <summary>
+ /// Provides basic thread synchronization functions similar to <see cref="WaitHandle"/>
+ /// </summary>
+ public interface IWaitHandle
+ {
+ /// <summary>
+ /// Waits for exclusive access to the resource indefinitly. If the signal is never received this method never returns
+ /// </summary>
+ /// <inheritdoc/>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ /// <returns>true if the current thread received the signal</returns>
+ public virtual bool WaitOne() => WaitOne(Timeout.Infinite);
+ /// <summary>
+ /// Waits for exclusive access to the resource until the specified number of milliseconds
+ /// </summary>
+ /// <param name="millisecondsTimeout">Time in milliseconds to wait for exclusive access to the resource</param>
+ /// <returns>true if the current thread received the signal, false if the timout expired, and access was not granted</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ bool WaitOne(int millisecondsTimeout);
+ /// <summary>
+ /// Waits for exclusive access to the resource until the specified <see cref="TimeSpan"/>
+ /// </summary>
+ /// <returns>true if the current thread received the signal, false if the timout expired, and access was not granted</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public virtual bool WaitOne(TimeSpan timeout) => WaitOne(Convert.ToInt32(timeout.TotalMilliseconds));
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/BitField.cs b/lib/Utils/src/BitField.cs
new file mode 100644
index 0000000..bc001df
--- /dev/null
+++ b/lib/Utils/src/BitField.cs
@@ -0,0 +1,115 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: BitField.cs
+*
+* BitField.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace VNLib.Utils
+{
+ /// <summary>
+ /// Represents a field of 64 bits that can be set or cleared using unsigned or signed masks
+ /// </summary>
+ public class BitField
+ {
+ private ulong Field;
+ /// <summary>
+ /// The readonly value of the <see cref="BitField"/>
+ /// </summary>
+ public ulong Value => Field;
+ /// <summary>
+ /// Creates a new <see cref="BitField"/> initialized to the specified value
+ /// </summary>
+ /// <param name="initial">Initial value</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public BitField(ulong initial) => Field = initial;
+ /// <summary>
+ /// Creates a new <see cref="BitField"/> initialized to the specified value
+ /// </summary>
+ /// <param name="initial">Initial value</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public BitField(long initial) => Field = unchecked((ulong)initial);
+ /// <summary>
+ /// Determines if the specified flag is set
+ /// </summary>
+ /// <param name="mask">The mask to compare against the field value</param>
+ /// <returns>True if the flag(s) is currently set, false if flag is not set</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IsSet(ulong mask) => (Field & mask) != 0;
+ /// <summary>
+ /// Determines if the specified flag is set
+ /// </summary>
+ /// <param name="mask">The mask to compare against the field value</param>
+ /// <returns>True if the flag(s) is currently set, false if flag is not set</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IsSet(long mask) => (Field & unchecked((ulong)mask)) != 0;
+ /// <summary>
+ /// Determines if the specified flag is set
+ /// </summary>
+ /// <param name="mask">The mask to compare against the field value</param>
+ /// <returns>True if the flag(s) is currently set, false if flag is not set</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Set(ulong mask) => Field |= mask;
+ /// <summary>
+ /// Determines if the specified flag is set
+ /// </summary>
+ /// <param name="mask">The mask to compare against the field value</param>
+ /// <returns>True if the flag(s) is currently set, false if flag is not set</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Set(long mask) => Field |= unchecked((ulong)mask);
+ /// <summary>
+ /// Sets or clears a flag(s) indentified by a mask based on the value
+ /// </summary>
+ /// <param name="mask">Mask used to identify flags</param>
+ /// <param name="value">True to set a flag, false to clear a flag</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Set(ulong mask, bool value)
+ {
+ if (value)
+ {
+ Set(mask);
+ }
+ else
+ {
+ Clear(mask);
+ }
+ }
+ /// <summary>
+ /// Clears the flag identified by the specified mask
+ /// </summary>
+ /// <param name="mask">The mask used to clear the given flag</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Clear(ulong mask) => Field &= ~mask;
+ /// <summary>
+ /// Clears the flag identified by the specified mask
+ /// </summary>
+ /// <param name="mask">The mask used to clear the given flag</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Clear(long mask) => Field &= ~unchecked((ulong)mask);
+ /// <summary>
+ /// Clears all flags by setting the <see cref="Field"/> property value to 0
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void ClearAll() => Field = 0;
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/ERRNO.cs b/lib/Utils/src/ERRNO.cs
new file mode 100644
index 0000000..972aa49
--- /dev/null
+++ b/lib/Utils/src/ERRNO.cs
@@ -0,0 +1,152 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ERRNO.cs
+*
+* ERRNO.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace VNLib.Utils
+{
+ /// <summary>
+ /// Implements a C style integer error code type. Size is platform dependent
+ /// </summary>
+ [StructLayout(LayoutKind.Sequential)]
+ public readonly struct ERRNO : IEquatable<ERRNO>, ISpanFormattable, IFormattable
+ {
+ /// <summary>
+ /// Represents a successfull error code (true)
+ /// </summary>
+ public static readonly ERRNO SUCCESS = true;
+
+ /// <summary>
+ /// Represents a failure error code (false)
+ /// </summary>
+ public static readonly ERRNO E_FAIL = false;
+
+ private readonly nint ErrorCode;
+ /// <summary>
+ /// Creates a new <see cref="ERRNO"/> from the specified error value
+ /// </summary>
+ /// <param name="errno">The value of the error to represent</param>
+ public ERRNO(nint errno) => ErrorCode = errno;
+ /// <summary>
+ /// Creates a new <see cref="ERRNO"/> from an <see cref="int"/> error code. null = 0 = false
+ /// </summary>
+ /// <param name="errorVal">Error code</param>
+ public static implicit operator ERRNO(int errorVal) => new (errorVal);
+ /// <summary>
+ /// Creates a new <see cref="ERRNO"/> from an <see cref="int"/> error code. null = 0 = false
+ /// </summary>
+ /// <param name="errorVal">Error code</param>
+ public static explicit operator ERRNO(int? errorVal) => new(errorVal ?? 0);
+ /// <summary>
+ /// Creates a new <see cref="ERRNO"/> from a booleam, 1 if true, 0 if false
+ /// </summary>
+ /// <param name="errorVal"></param>
+ public static implicit operator ERRNO(bool errorVal) => new(errorVal ? 1 : 0);
+ /// <summary>
+ /// Creates a new <see cref="ERRNO"/> from a pointer value
+ /// </summary>
+ /// <param name="errno">The pointer value representing an error code</param>
+ public static implicit operator ERRNO(nint errno) => new(errno);
+ /// <summary>
+ /// Error value as integer. Value of supplied error code or if cast from boolean 1 if true, 0 if false
+ /// </summary>
+ /// <param name="errorVal"><see cref="ERRNO"/> to get error code from</param>
+ public static implicit operator int(ERRNO errorVal) => (int)errorVal.ErrorCode;
+ /// <summary>
+ /// C style boolean conversion. false if 0, true otherwise
+ /// </summary>
+ /// <param name="errorVal"></param>
+ public static implicit operator bool(ERRNO errorVal) => errorVal != 0;
+ /// <summary>
+ /// Creates a new <see cref="IntPtr"/> from the value if the stored (nint) error code
+ /// </summary>
+ /// <param name="errno">The <see cref="ERRNO"/> contating the pointer value</param>
+ public static implicit operator IntPtr(ERRNO errno) => new(errno.ErrorCode);
+ /// <summary>
+ /// Creates a new <c>nint</c> from the value if the stored error code
+ /// </summary>
+ /// <param name="errno">The <see cref="ERRNO"/> contating the pointer value</param>
+ public static implicit operator nint(ERRNO errno) => errno.ErrorCode;
+
+ public static ERRNO operator +(ERRNO err, int add) => new(err.ErrorCode + add);
+ public static ERRNO operator +(ERRNO err, nint add) => new(err.ErrorCode + add);
+ public static ERRNO operator ++(ERRNO err) => new(err.ErrorCode + 1);
+ public static ERRNO operator --(ERRNO err) => new(err.ErrorCode - 1);
+ public static ERRNO operator -(ERRNO err, int subtract) => new(err.ErrorCode - subtract);
+ public static ERRNO operator -(ERRNO err, nint subtract) => new(err.ErrorCode - subtract);
+
+ public static bool operator >(ERRNO err, ERRNO other) => err.ErrorCode > other.ErrorCode;
+ public static bool operator <(ERRNO err, ERRNO other) => err.ErrorCode < other.ErrorCode;
+ public static bool operator >=(ERRNO err, ERRNO other) => err.ErrorCode >= other.ErrorCode;
+ public static bool operator <=(ERRNO err, ERRNO other) => err.ErrorCode <= other.ErrorCode;
+
+ public static bool operator >(ERRNO err, int other) => err.ErrorCode > other;
+ public static bool operator <(ERRNO err, int other) => err.ErrorCode < other;
+ public static bool operator >=(ERRNO err, int other) => err.ErrorCode >= other;
+ public static bool operator <=(ERRNO err, int other) => err.ErrorCode <= other;
+
+ public static bool operator >(ERRNO err, nint other) => err.ErrorCode > other;
+ public static bool operator <(ERRNO err, nint other) => err.ErrorCode < other;
+ public static bool operator >=(ERRNO err, nint other) => err.ErrorCode >= other;
+ public static bool operator <=(ERRNO err, nint other) => err.ErrorCode <= other;
+
+ public static bool operator ==(ERRNO err, ERRNO other) => err.ErrorCode == other.ErrorCode;
+ public static bool operator !=(ERRNO err, ERRNO other) => err.ErrorCode != other.ErrorCode;
+ public static bool operator ==(ERRNO err, int other) => err.ErrorCode == other;
+ public static bool operator !=(ERRNO err, int other) => err.ErrorCode != other;
+ public static bool operator ==(ERRNO err, nint other) => err.ErrorCode == other;
+ public static bool operator !=(ERRNO err, nint other) => err.ErrorCode != other;
+
+ public readonly bool Equals(ERRNO other) => ErrorCode == other.ErrorCode;
+ public readonly override bool Equals(object? obj) => obj is ERRNO other && Equals(other);
+ public readonly override int GetHashCode() => ErrorCode.GetHashCode();
+
+ /// <summary>
+ /// The integer error value of the current instance in radix 10
+ /// </summary>
+ /// <returns></returns>
+ public readonly override string ToString()
+ {
+ //Return the string of the error code number
+ return ErrorCode.ToString();
+ }
+ public readonly string ToString(string format)
+ {
+ //Return the string of the error code number
+ return ErrorCode.ToString(format);
+ }
+
+ ///<inheritdoc/>
+ public readonly bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider provider)
+ {
+ return ErrorCode.TryFormat(destination, out charsWritten, format, provider);
+ }
+ ///<inheritdoc/>
+ public readonly string ToString(string format, IFormatProvider formatProvider)
+ {
+ return ErrorCode.ToString(format, formatProvider);
+ }
+ }
+}
diff --git a/lib/Utils/src/Extensions/CacheExtensions.cs b/lib/Utils/src/Extensions/CacheExtensions.cs
new file mode 100644
index 0000000..665e282
--- /dev/null
+++ b/lib/Utils/src/Extensions/CacheExtensions.cs
@@ -0,0 +1,407 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: CacheExtensions.cs
+*
+* CacheExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+using VNLib.Utils.Memory.Caching;
+
+namespace VNLib.Utils.Extensions
+{
+ /// <summary>
+ /// Cache collection extensions
+ /// </summary>
+ public static class CacheExtensions
+ {
+ /// <summary>
+ /// <para>
+ /// Stores a new record. If an old record exists, the records are compared,
+ /// if they are not equal, the old record is evicted and the new record is stored
+ /// </para>
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="T">A cachable object</typeparam>
+ /// <param name="store"></param>
+ /// <param name="key">The unique key identifying the record</param>
+ /// <param name="record">The record to store</param>
+ /// <remarks>
+ /// Locks on the store parameter to provide mutual exclusion for non thread-safe
+ /// data structures.
+ /// </remarks>
+ public static void StoreRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key, T record) where T : ICacheable
+ {
+ if (store is null)
+ {
+ throw new ArgumentNullException(nameof(store));
+ }
+
+ T ?oldRecord = default;
+ lock (store)
+ {
+ //See if an old record exists
+ if (!store.Remove(key, out oldRecord) || oldRecord == null)
+ {
+ //Old record doesnt exist, store and return
+ store[key] = record;
+ return;
+ }
+ //See if the old and new records and the same record
+ if (oldRecord.Equals(record))
+ {
+ //records are equal, so we can exit
+ return;
+ }
+ //Old record is not equal, so we can store the new record and evict the old on
+ store[key] = record;
+ }
+ //Call evict on the old record
+ oldRecord.Evicted();
+ }
+ /// <summary>
+ /// <para>
+ /// Stores a new record and updates the expiration date. If an old record exists, the records
+ /// are compared, if they are not equal, the old record is evicted and the new record is stored
+ /// </para>
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="T">A cachable object</typeparam>
+ /// <param name="store"></param>
+ /// <param name="key">The unique key identifying the record</param>
+ /// <param name="record">The record to store</param>
+ /// <param name="validFor">The new expiration time of the record</param>
+ /// <remarks>
+ /// Locks on the store parameter to provide mutual exclusion for non thread-safe
+ /// data structures.
+ /// </remarks>
+ public static void StoreRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key, T record, TimeSpan validFor) where T : ICacheable
+ {
+ //Update the expiration time
+ record.Expires = DateTime.UtcNow.Add(validFor);
+ //Store
+ StoreRecord(store, key, record);
+ }
+ /// <summary>
+ /// <para>
+ /// Returns a stored record if it exists and is not expired. If the record exists
+ /// but has expired, it is evicted.
+ /// </para>
+ /// <para>
+ /// If a record is evicted, the return value evaluates to -1 and the value parameter
+ /// is set to the old record if the caller wished to inspect the record after the
+ /// eviction method completes
+ /// </para>
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="T">A cachable object</typeparam>
+ /// <param name="store"></param>
+ /// <param name="key"></param>
+ /// <param name="value">The record</param>
+ /// <returns>
+ /// Gets a value indicating the reults of the operation. 0 if the record is not found, -1 if expired, 1 if
+ /// record is valid
+ /// </returns>
+ /// <remarks>
+ /// Locks on the store parameter to provide mutual exclusion for non thread-safe
+ /// data structures.
+ /// </remarks>
+ public static ERRNO TryGetOrEvictRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key, out T? value) where T : ICacheable
+ {
+ if (store is null)
+ {
+ throw new ArgumentNullException(nameof(store));
+ }
+
+ value = default;
+ //Cache current date time before entering the lock
+ DateTime now = DateTime.UtcNow;
+ //Get value
+ lock (store)
+ {
+ //try to get the value
+ if (!store.TryGetValue(key, out value))
+ {
+ //not found
+ return 0;
+ }
+ //Not expired
+ if (value.Expires > now)
+ {
+ return true;
+ }
+ //Remove from store
+ _ = store.Remove(key);
+ }
+ //Call the evict func
+ value.Evicted();
+ return -1;
+ }
+ /// <summary>
+ /// Updates the expiration date on a record to the specified time if it exists, regardless
+ /// of its validity
+ /// </summary>
+ /// <typeparam name="TKey">Diction key type</typeparam>
+ /// <typeparam name="T">A cachable object</typeparam>
+ /// <param name="store"></param>
+ /// <param name="key">The unique key identifying the record to update</param>
+ /// <param name="extendedTime">The expiration time (time added to <see cref="DateTime.UtcNow"/>)</param>
+ /// <remarks>
+ /// Locks on the store parameter to provide mutual exclusion for non thread-safe
+ /// data structures.
+ /// </remarks>
+ public static void UpdateRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key, TimeSpan extendedTime) where T : ICacheable
+ {
+ //Cacl the expiration time
+ DateTime expiration = DateTime.UtcNow.Add(extendedTime);
+ lock (store)
+ {
+ //Update the expiration time if the record exists
+ if (store.TryGetValue(key, out T? record) && record != null)
+ {
+ record.Expires = expiration;
+ }
+ }
+ }
+ /// <summary>
+ /// Evicts a stored record from the store. If the record is found, the eviction
+ /// method is executed
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="store"></param>
+ /// <param name="key">The unique key identifying the record</param>
+ /// <returns>True if the record was found and evicted</returns>
+ public static bool EvictRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key) where T : ICacheable
+ {
+ if (store is null)
+ {
+ throw new ArgumentNullException(nameof(store));
+ }
+
+ T? record = default;
+ lock (store)
+ {
+ //Try to remove the record
+ if (!store.Remove(key, out record) || record == null)
+ {
+ //No record found or null
+ return false;
+ }
+ }
+ //Call eviction mode
+ record.Evicted();
+ return true;
+ }
+ /// <summary>
+ /// Evicts all expired records from the store
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="T"></typeparam>
+ public static void CollectRecords<TKey, T>(this IDictionary<TKey, T> store) where T : ICacheable
+ {
+ CollectRecords(store, DateTime.UtcNow);
+ }
+
+ /// <summary>
+ /// Evicts all expired records from the store
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="store"></param>
+ /// <param name="validAfter">A time that specifies the time which expired records should be evicted</param>
+ public static void CollectRecords<TKey, T>(this IDictionary<TKey, T> store, DateTime validAfter) where T : ICacheable
+ {
+ if (store is null)
+ {
+ throw new ArgumentNullException(nameof(store));
+ }
+ //Build a query to get the keys that belong to the expired records
+ IEnumerable<KeyValuePair<TKey, T>> expired = store.Where(s => s.Value.Expires < validAfter);
+ //temp list for expired records
+ IEnumerable<T> evicted;
+ //Take lock on store
+ lock (store)
+ {
+ KeyValuePair<TKey, T>[] kvp = expired.ToArray();
+ //enumerate to array so values can be removed while the lock is being held
+ foreach (KeyValuePair<TKey, T> pair in kvp)
+ {
+ //remove the record and call the eviction method
+ _ = store.Remove(pair);
+ }
+ //select values while lock held
+ evicted = kvp.Select(static v => v.Value);
+ }
+ //Iterrate over evicted records and call evicted method
+ foreach (T ev in evicted)
+ {
+ ev.Evicted();
+ }
+ }
+
+ /// <summary>
+ /// Allows for mutually exclusive use of a <see cref="ICacheable"/> record with a
+ /// state parameter
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="T"></typeparam>
+ /// <typeparam name="TState"></typeparam>
+ /// <param name="store"></param>
+ /// <param name="key">The unique key identifying the record</param>
+ /// <param name="state">A user-token type state parameter to pass to the use callback method</param>
+ /// <param name="useCtx">A callback method that will be passed the record to use within an exclusive context</param>
+ public static void UseRecord<TKey, T, TState>(this IDictionary<TKey, T> store, TKey key, TState state, Action<T, TState> useCtx) where T: ICacheable
+ {
+ if (store is null)
+ {
+ throw new ArgumentNullException(nameof(store));
+ }
+
+ if (useCtx is null)
+ {
+ throw new ArgumentNullException(nameof(useCtx));
+ }
+
+ lock (store)
+ {
+ //If the record exists
+ if(store.TryGetValue(key, out T? record))
+ {
+ //Use it within the lock statement
+ useCtx(record, state);
+ }
+ }
+ }
+ /// <summary>
+ /// Allows for mutually exclusive use of a <see cref="ICacheable"/>
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="store"></param>
+ /// <param name="key">The unique key identifying the record</param>
+ /// <param name="useCtx">A callback method that will be passed the record to use within an exclusive context</param>
+ public static void UseRecord<TKey, T>(this IDictionary<TKey, T> store, TKey key, Action<T> useCtx) where T : ICacheable
+ {
+ if (store is null)
+ {
+ throw new ArgumentNullException(nameof(store));
+ }
+
+ if (useCtx is null)
+ {
+ throw new ArgumentNullException(nameof(useCtx));
+ }
+
+ lock (store)
+ {
+ //If the record exists
+ if (store.TryGetValue(key, out T? record))
+ {
+ //Use it within the lock statement
+ useCtx(record);
+ }
+ }
+ }
+ /// <summary>
+ /// Allows for mutually exclusive use of a <see cref="ICacheable"/> record with a
+ /// state parameter, only if the found record is valid
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="T"></typeparam>
+ /// <typeparam name="TState"></typeparam>
+ /// <param name="store"></param>
+ /// <param name="key">The unique key identifying the record</param>
+ /// <param name="state">A user-token type state parameter to pass to the use callback method</param>
+ /// <param name="useCtx">A callback method that will be passed the record to use within an exclusive context</param>
+ /// <remarks>If the record is found, but is expired, the record is evicted from the store. The callback is never invoked</remarks>
+ public static void UseIfValid<TKey, T, TState>(this IDictionary<TKey, T> store, TKey key, TState state, Action<T, TState> useCtx) where T : ICacheable
+ {
+ if (store is null)
+ {
+ throw new ArgumentNullException(nameof(store));
+ }
+
+ if (useCtx is null)
+ {
+ throw new ArgumentNullException(nameof(useCtx));
+ }
+
+ DateTime now = DateTime.UtcNow;
+ T? record;
+ lock (store)
+ {
+ //If the record exists, check if its valid
+ if (store.TryGetValue(key, out record) && record.Expires < now)
+ {
+ //Use it within the lock statement
+ useCtx(record, state);
+ return;
+ }
+ //Record is no longer valid
+ _ = store.Remove(key);
+ }
+ //Call evicted method
+ record?.Evicted();
+ }
+ /// <summary>
+ /// Allows for mutually exclusive use of a <see cref="ICacheable"/> record with a
+ /// state parameter, only if the found record is valid
+ /// </summary>
+ /// <typeparam name="TKey"></typeparam>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="store"></param>
+ /// <param name="key">The unique key identifying the record</param>
+ /// <param name="useCtx">A callback method that will be passed the record to use within an exclusive context</param>
+ /// <remarks>If the record is found, but is expired, the record is evicted from the store. The callback is never invoked</remarks>
+ public static void UseIfValid<TKey, T>(this IDictionary<TKey, T> store, TKey key, Action<T> useCtx) where T : ICacheable
+ {
+ if (store is null)
+ {
+ throw new ArgumentNullException(nameof(store));
+ }
+
+ if (useCtx is null)
+ {
+ throw new ArgumentNullException(nameof(useCtx));
+ }
+
+ DateTime now = DateTime.UtcNow;
+ T? record;
+ lock (store)
+ {
+ //If the record exists, check if its valid
+ if (store.TryGetValue(key, out record) && record.Expires < now)
+ {
+ //Use it within the lock statement
+ useCtx(record);
+ return;
+ }
+ //Record is no longer valid
+ _ = store.Remove(key);
+ }
+ //Call evicted method
+ record?.Evicted();
+ }
+ }
+}
diff --git a/lib/Utils/src/Extensions/CollectionExtensions.cs b/lib/Utils/src/Extensions/CollectionExtensions.cs
new file mode 100644
index 0000000..7636cd3
--- /dev/null
+++ b/lib/Utils/src/Extensions/CollectionExtensions.cs
@@ -0,0 +1,100 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: CollectionExtensions.cs
+*
+* CollectionExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Collections.Generic;
+
+namespace VNLib.Utils.Extensions
+{
+ /// <summary>
+ /// Provides collection extension methods
+ /// </summary>
+ public static class CollectionExtensions
+ {
+ /// <summary>
+ /// Gets a previously-stored base32 encoded value-type from the lookup and returns its initialized structure from
+ /// the value stored
+ /// </summary>
+ /// <typeparam name="TKey">The key type used to index the lookup</typeparam>
+ /// <typeparam name="TValue">An unmanaged structure type</typeparam>
+ /// <param name="lookup"></param>
+ /// <param name="key">The key used to identify the value</param>
+ /// <returns>The initialized structure, or default if the lookup returns null/empty string</returns>
+ public static TValue GetValueType<TKey, TValue>(this IIndexable<TKey, string> lookup, TKey key) where TValue : unmanaged where TKey : notnull
+ {
+ if (lookup is null)
+ {
+ throw new ArgumentNullException(nameof(lookup));
+ }
+ //Get value
+ string value = lookup[key];
+ //If the string is set, recover the value and return it
+ return string.IsNullOrWhiteSpace(value) ? default : VnEncoding.FromBase32String<TValue>(value);
+ }
+
+ /// <summary>
+ /// Serializes a value-type in base32 encoding and stores it at the specified key
+ /// </summary>
+ /// <typeparam name="TKey">The key type used to index the lookup</typeparam>
+ /// <typeparam name="TValue">An unmanaged structure type</typeparam>
+ /// <param name="lookup"></param>
+ /// <param name="key">The key used to identify the value</param>
+ /// <param name="value">The value to serialze</param>
+ public static void SetValueType<TKey, TValue>(this IIndexable<TKey, string> lookup, TKey key, TValue value) where TValue : unmanaged where TKey : notnull
+ {
+ //encode string from value type and store in lookup
+ lookup[key] = VnEncoding.ToBase32String(value);
+ }
+ /// <summary>
+ /// Executes a handler delegate on every element of the list within a try-catch block
+ /// and rethrows exceptions as an <see cref="AggregateException"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="list"></param>
+ /// <param name="handler">An <see cref="Action"/> handler delegate to complete some operation on the elements within the list</param>
+ /// <exception cref="AggregateException"></exception>
+ public static void TryForeach<T>(this IEnumerable<T> list, Action<T> handler)
+ {
+ List<Exception>? exceptionList = null;
+ foreach(T item in list)
+ {
+ try
+ {
+ handler(item);
+ }
+ catch(Exception ex)
+ {
+ //Init new list and add the exception
+ exceptionList ??= new();
+ exceptionList.Add(ex);
+ }
+ }
+ //Raise aggregate exception for all caught exceptions
+ if(exceptionList?.Count > 0)
+ {
+ throw new AggregateException(exceptionList);
+ }
+ }
+ }
+}
diff --git a/lib/Utils/src/Extensions/IoExtensions.cs b/lib/Utils/src/Extensions/IoExtensions.cs
new file mode 100644
index 0000000..baba7dc
--- /dev/null
+++ b/lib/Utils/src/Extensions/IoExtensions.cs
@@ -0,0 +1,401 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IoExtensions.cs
+*
+* IoExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Buffers;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Runtime.Versioning;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils.IO;
+using VNLib.Utils.Memory;
+
+using static VNLib.Utils.Memory.Memory;
+
+namespace VNLib.Utils.Extensions
+{
+ /// <summary>
+ /// Provieds extension methods for common IO operations
+ /// </summary>
+ public static class IoExtensions
+ {
+ /// <summary>
+ /// Unlocks the entire file
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [UnsupportedOSPlatform("ios")]
+ [UnsupportedOSPlatform("macos")]
+ [UnsupportedOSPlatform("tvos")]
+ public static void Unlock(this FileStream fs)
+ {
+ _ = fs ?? throw new ArgumentNullException(nameof(fs));
+ //Unlock the entire file
+ fs.Unlock(0, fs.Length);
+ }
+
+ /// <summary>
+ /// Locks the entire file
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [UnsupportedOSPlatform("ios")]
+ [UnsupportedOSPlatform("macos")]
+ [UnsupportedOSPlatform("tvos")]
+ public static void Lock(this FileStream fs)
+ {
+ _ = fs ?? throw new ArgumentNullException(nameof(fs));
+ //Lock the entire length of the file
+ fs.Lock(0, fs.Length);
+ }
+
+ /// <summary>
+ /// Provides an async wrapper for copying data from the current stream to another using an unmanged
+ /// buffer.
+ /// </summary>
+ /// <param name="source"></param>
+ /// <param name="dest">The destination data stream to write data to</param>
+ /// <param name="bufferSize">The size of the buffer to use while copying data. (Value will be clamped to the size of the stream if seeking is available)</param>
+ /// <param name="heap">The <see cref="IUnmangedHeap"/> to allocate the buffer from</param>
+ /// <param name="token">A token that may cancel asynchronous operations</param>
+ /// <returns>A <see cref="ValueTask"/> that completes when the copy operation has completed</returns>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="ArgumentException"></exception>
+ public static async ValueTask CopyToAsync(this Stream source, Stream dest, int bufferSize, IUnmangedHeap heap, CancellationToken token = default)
+ {
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (heap is null)
+ {
+ throw new ArgumentNullException(nameof(heap));
+ }
+
+ if (source.CanSeek)
+ {
+ bufferSize = (int)Math.Min(source.Length, bufferSize);
+ }
+ //Alloc a buffer
+ using IMemoryOwner<byte> buffer = heap.DirectAlloc<byte>(bufferSize);
+ //Wait for copy to complete
+ await CopyToAsync(source, dest, buffer.Memory, token);
+ }
+ /// <summary>
+ /// Provides an async wrapper for copying data from the current stream to another with a
+ /// buffer from the <paramref name="heap"/>
+ /// </summary>
+ /// <param name="source"></param>
+ /// <param name="dest">The destination data stream to write data to</param>
+ /// <param name="bufferSize">The size of the buffer to use while copying data. (Value will be clamped to the size of the stream if seeking is available)</param>
+ /// <param name="count">The number of bytes to copy from the current stream to destination stream</param>
+ /// <param name="heap">The heap to alloc buffer from</param>
+ /// <param name="token">A token that may cancel asynchronous operations</param>
+ /// <returns>A <see cref="ValueTask"/> that completes when the copy operation has completed</returns>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="ArgumentException"></exception>
+ public static async ValueTask CopyToAsync(this Stream source, Stream dest, long count, int bufferSize, IUnmangedHeap heap, CancellationToken token = default)
+ {
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (source.CanSeek)
+ {
+ bufferSize = (int)Math.Min(source.Length, bufferSize);
+ }
+ //Alloc a buffer
+ using IMemoryOwner<byte> buffer = heap.DirectAlloc<byte>(bufferSize);
+ //Wait for copy to complete
+ await CopyToAsync(source, dest, buffer.Memory, count, token);
+ }
+
+ /// <summary>
+ /// Copies data from one stream to another, using self managed buffers. May allocate up to 2MB.
+ /// </summary>
+ /// <param name="source">Source stream to read from</param>
+ /// <param name="dest">Destination stream to write data to</param>
+ /// <param name="heap">The heap to allocate buffers from</param>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static void CopyTo(this Stream source, Stream dest, IUnmangedHeap? heap = null)
+ {
+ if (dest is null)
+ {
+ throw new ArgumentNullException(nameof(dest));
+ }
+
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (!source.CanRead)
+ {
+ throw new ArgumentException("Source stream is unreadable", nameof(source));
+ }
+
+ if (!dest.CanWrite)
+ {
+ throw new ArgumentException("Destination stream is unwritable", nameof(dest));
+ }
+ heap ??= Shared;
+ //Get a buffer size, maximum of 2mb buffer size if the stream supports seeking, otherwise, min buf size
+ int bufSize = source.CanSeek ? (int)Math.Min(source.Length, MAX_BUF_SIZE) : MIN_BUF_SIZE;
+ //Length must be 0, so return
+ if (bufSize == 0)
+ {
+ return;
+ }
+ //Alloc a buffer
+ using UnsafeMemoryHandle<byte> buffer = heap.UnsafeAlloc<byte>(bufSize);
+ int read;
+ do
+ {
+ //read
+ read = source.Read(buffer.Span);
+ //Guard
+ if (read == 0)
+ {
+ break;
+ }
+ //write only the data that was read (slice)
+ dest.Write(buffer.Span[..read]);
+ } while (true);
+ }
+ /// <summary>
+ /// Copies data from one stream to another, using self managed buffers. May allocate up to 2MB.
+ /// </summary>
+ /// <param name="source">Source stream to read from</param>
+ /// <param name="dest">Destination stream to write data to</param>
+ /// <param name="count">Number of bytes to read/write</param>
+ /// <param name="heap">The heap to allocate buffers from</param>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static void CopyTo(this Stream source, Stream dest, long count, IUnmangedHeap? heap = null)
+ {
+ if (dest is null)
+ {
+ throw new ArgumentNullException(nameof(dest));
+ }
+
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (!source.CanRead)
+ {
+ throw new ArgumentException("Source stream is unreadable", nameof(source));
+ }
+ if (!dest.CanWrite)
+ {
+ throw new ArgumentException("Destination stream is unwritable", nameof(dest));
+ }
+ //Set default heap
+ heap ??= Shared;
+ //Get a buffer size, maximum of 2mb buffer size if the stream supports seeking, otherwise, min buf size
+ int bufSize = source.CanSeek ? (int)Math.Min(source.Length, MAX_BUF_SIZE) : MIN_BUF_SIZE;
+ //Length must be 0, so return
+ if (bufSize == 0)
+ {
+ return;
+ }
+ //Alloc a buffer
+ using UnsafeMemoryHandle<byte> buffer = heap.UnsafeAlloc<byte>(bufSize);
+ //wrapper around offset pointer
+ long total = 0;
+ int read;
+ do
+ {
+ Span<byte> wrapper = buffer.Span[..(int)Math.Min(bufSize, (count - total))];
+ //read
+ read = source.Read(wrapper);
+ //Guard
+ if (read == 0)
+ {
+ break;
+ }
+ //write only the data that was read (slice)
+ dest.Write(wrapper[..read]);
+ //Update total
+ total += read;
+ } while (true);
+ }
+
+ /// <summary>
+ /// Copies data from the current stream to the destination stream using the supplied memory buffer
+ /// </summary>
+ /// <param name="source"></param>
+ /// <param name="dest">The destination data stream to write data to</param>
+ /// <param name="buffer">The buffer to use when copying data</param>
+ /// <param name="token">A token that may cancel asynchronous operations</param>
+ /// <returns>A <see cref="ValueTask"/> that completes when the copy operation has completed</returns>
+ /// <exception cref="ArgumentException"></exception>
+ public static async ValueTask CopyToAsync(this Stream source, Stream dest, Memory<byte> buffer, CancellationToken token = default)
+ {
+ if (dest is null)
+ {
+ throw new ArgumentNullException(nameof(dest));
+ }
+
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ //Make sure source can be read from, and dest can be written to
+ if (!source.CanRead)
+ {
+ throw new ArgumentException("Source stream is unreadable", nameof(source));
+ }
+ if (!dest.CanWrite)
+ {
+ throw new ArgumentException("Destination stream is unwritable", nameof(dest));
+ }
+ //Read in loop
+ int read;
+ while (true)
+ {
+ //read
+ read = await source.ReadAsync(buffer, token);
+ //Guard
+ if (read == 0)
+ {
+ break;
+ }
+ //write only the data that was read (slice)
+ await dest.WriteAsync(buffer[..read], token);
+ }
+ }
+
+ /// <summary>
+ /// Copies data from the current stream to the destination stream using the supplied memory buffer
+ /// </summary>
+ /// <param name="source"></param>
+ /// <param name="dest">The destination data stream to write data to</param>
+ /// <param name="buffer">The buffer to use when copying data</param>
+ /// <param name="count">The number of bytes to copy from the current stream to destination stream</param>
+ /// <param name="token">A token that may cancel asynchronous operations</param>
+ /// <returns>A <see cref="ValueTask"/> that completes when the copy operation has completed</returns>
+ /// <exception cref="ArgumentException"></exception>
+ public static async ValueTask CopyToAsync(this Stream source, Stream dest, Memory<byte> buffer, long count, CancellationToken token = default)
+ {
+ if (dest is null)
+ {
+ throw new ArgumentNullException(nameof(dest));
+ }
+
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ //Make sure source can be read from, and dest can be written to
+ if (!source.CanRead)
+ {
+ throw new ArgumentException("Source stream is unreadable", nameof(source));
+ }
+ if (!dest.CanWrite)
+ {
+ throw new ArgumentException("Destination stream is unwritable", nameof(dest));
+ }
+ /*
+ * Track total count so we copy the exect number of
+ * bytes from the source
+ */
+ long total = 0;
+ int bufferSize = buffer.Length;
+ int read;
+ while (true)
+ {
+ //get offset wrapper of the total buffer or remaining count
+ Memory<byte> offset = buffer[..(int)Math.Min(bufferSize, count - total)];
+ //read
+ read = await source.ReadAsync(offset, token);
+ //Guard
+ if (read == 0)
+ {
+ break;
+ }
+ //write only the data that was read (slice)
+ await dest.WriteAsync(offset[..read], token);
+ //Update total
+ total += read;
+ }
+ }
+
+ /// <summary>
+ /// Opens a file within the current directory
+ /// </summary>
+ /// <param name="dir"></param>
+ /// <param name="fileName">The name of the file to open</param>
+ /// <param name="mode">The <see cref="FileMode"/> to open the file with</param>
+ /// <param name="access">The <see cref="FileAccess"/> to open the file with</param>
+ /// <param name="share"></param>
+ /// <param name="bufferSize">The size of the buffer to read/write with</param>
+ /// <param name="options"></param>
+ /// <returns>The <see cref="FileStream"/> of the opened file</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static FileStream OpenFile(this DirectoryInfo dir,
+ string fileName,
+ FileMode mode,
+ FileAccess access,
+ FileShare share = FileShare.None,
+ int bufferSize = 4096,
+ FileOptions options = FileOptions.None)
+ {
+ _ = dir ?? throw new ArgumentNullException(nameof(dir));
+ string fullPath = Path.Combine(dir.FullName, fileName);
+ return new FileStream(fullPath, mode, access, share, bufferSize, options);
+ }
+ /// <summary>
+ /// Deletes the speicifed file from the current directory
+ /// </summary>
+ /// <param name="dir"></param>
+ /// <param name="fileName">The name of the file to delete</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void DeleteFile(this DirectoryInfo dir, string fileName)
+ {
+ _ = dir ?? throw new ArgumentNullException(nameof(dir));
+ string fullPath = Path.Combine(dir.FullName, fileName);
+ File.Delete(fullPath);
+ }
+ /// <summary>
+ /// Determines if a file exists within the current directory
+ /// </summary>
+ /// <param name="dir"></param>
+ /// <param name="fileName">The name of the file to search for</param>
+ /// <returns>True if the file is found and the user has permission to access the file, false otherwise</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool FileExists(this DirectoryInfo dir, string fileName)
+ {
+ _ = dir ?? throw new ArgumentNullException(nameof(dir));
+ string fullPath = Path.Combine(dir.FullName, fileName);
+ return FileOperations.FileExists(fullPath);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Extensions/JsonExtensions.cs b/lib/Utils/src/Extensions/JsonExtensions.cs
new file mode 100644
index 0000000..a27dcc0
--- /dev/null
+++ b/lib/Utils/src/Extensions/JsonExtensions.cs
@@ -0,0 +1,215 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: JsonExtensions.cs
+*
+* JsonExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Text.Json;
+using System.Collections.Generic;
+
+using VNLib.Utils.IO;
+
+namespace VNLib.Utils.Extensions
+{
+ /// <summary>
+ /// Specifies how to parse a timespan value from a <see cref="JsonDocument"/> element
+ /// </summary>
+ public enum TimeParseType
+ {
+ Milliseconds,
+ Seconds,
+ Minutes,
+ Hours,
+ Days,
+ Ticks
+ }
+
+ public static class JsonExtensions
+ {
+ /// <summary>
+ /// Converts a JSON encoded string to an object of the specified type
+ /// </summary>
+ /// <typeparam name="T">Output type of the object</typeparam>
+ /// <param name="value"></param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during de-serialization</param>
+ /// <returns>The new object or default if the string is null or empty</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static T? AsJsonObject<T>(this string value, JsonSerializerOptions? options = null)
+ {
+ return !string.IsNullOrWhiteSpace(value) ? JsonSerializer.Deserialize<T>(value, options) : default;
+ }
+ /// <summary>
+ /// Converts a JSON encoded binary data to an object of the specified type
+ /// </summary>
+ /// <typeparam name="T">Output type of the object</typeparam>
+ /// <param name="utf8bin"></param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during de-serialization</param>
+ /// <returns>The new object or default if the string is null or empty</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static T? AsJsonObject<T>(this in ReadOnlySpan<byte> utf8bin, JsonSerializerOptions? options = null)
+ {
+ return utf8bin.IsEmpty ? default : JsonSerializer.Deserialize<T>(utf8bin, options);
+ }
+ /// <summary>
+ /// Converts a JSON encoded binary data to an object of the specified type
+ /// </summary>
+ /// <typeparam name="T">Output type of the object</typeparam>
+ /// <param name="utf8bin"></param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during de-serialization</param>
+ /// <returns>The new object or default if the string is null or empty</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static T? AsJsonObject<T>(this in ReadOnlyMemory<byte> utf8bin, JsonSerializerOptions? options = null)
+ {
+ return utf8bin.IsEmpty ? default : JsonSerializer.Deserialize<T>(utf8bin.Span, options);
+ }
+ /// <summary>
+ /// Converts a JSON encoded binary data to an object of the specified type
+ /// </summary>
+ /// <typeparam name="T">Output type of the object</typeparam>
+ /// <param name="utf8bin"></param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during de-serialization</param>
+ /// <returns>The new object or default if the string is null or empty</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static T? AsJsonObject<T>(this byte[] utf8bin, JsonSerializerOptions? options = null)
+ {
+ return utf8bin == null ? default : JsonSerializer.Deserialize<T>(utf8bin.AsSpan(), options);
+ }
+ /// <summary>
+ /// Parses a json encoded string to a json documen
+ /// </summary>
+ /// <param name="jsonString"></param>
+ /// <param name="options"></param>
+ /// <returns>If the json string is null, returns null, otherwise the json document around the data</returns>
+ /// <exception cref="JsonException"></exception>
+ public static JsonDocument? AsJsonDocument(this string jsonString, JsonDocumentOptions options = default)
+ {
+ return jsonString == null ? null : JsonDocument.Parse(jsonString, options);
+ }
+ /// <summary>
+ /// Shortcut extension to <see cref="JsonElement.GetProperty(string)"/> and returns a string
+ /// </summary>
+ /// <param name="element"></param>
+ /// <param name="propertyName">The name of the property to get the string value of</param>
+ /// <returns>If the property exists, returns the string stored at that property</returns>
+ public static string? GetPropString(this in JsonElement element, string propertyName)
+ {
+ return element.TryGetProperty(propertyName, out JsonElement el) ? el.GetString() : null;
+ }
+ /// <summary>
+ /// Shortcut extension to <see cref="JsonElement.GetProperty(string)"/> and returns a string
+ /// </summary>
+ /// <param name="conf"></param>
+ /// <param name="propertyName">The name of the property to get the string value of</param>
+ /// <returns>If the property exists, returns the string stored at that property</returns>
+ public static string? GetPropString(this IReadOnlyDictionary<string, JsonElement> conf, string propertyName)
+ {
+ return conf.TryGetValue(propertyName, out JsonElement el) ? el.GetString() : null;
+ }
+
+ /// <summary>
+ /// Shortcut extension to <see cref="JsonElement.GetProperty(string)"/> and returns a string
+ /// </summary>
+ /// <param name="conf"></param>
+ /// <param name="propertyName">The name of the property to get the string value of</param>
+ /// <returns>If the property exists, returns the string stored at that property</returns>
+ public static string? GetPropString(this IDictionary<string, JsonElement> conf, string propertyName)
+ {
+ return conf.TryGetValue(propertyName, out JsonElement el) ? el.GetString() : null;
+ }
+
+ /// <summary>
+ /// Attemts to serialze an object to a JSON encoded string
+ /// </summary>
+ /// <param name="obj"></param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> to use during serialization</param>
+ /// <returns>A JSON encoded string of the serialized object, or null if the object is null</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ public static string? ToJsonString<T>(this T obj, JsonSerializerOptions? options = null)
+ {
+ return obj == null ? null : JsonSerializer.Serialize(obj, options);
+ }
+
+ /// <summary>
+ /// Merges the current <see cref="JsonDocument"/> with another <see cref="JsonDocument"/> to
+ /// create a new document of combined properties
+ /// </summary>
+ /// <param name="initial"></param>
+ /// <param name="other">The <see cref="JsonDocument"/> to combine with the first document</param>
+ /// <param name="initalName">The name of the new element containing the initial document data</param>
+ /// <param name="secondName">The name of the new element containing the additional document data</param>
+ /// <returns>A new document with a parent root containing the combined objects</returns>
+ public static JsonDocument Merge(this JsonDocument initial, JsonDocument other, string initalName, string secondName)
+ {
+ //Open a new memory buffer
+ using VnMemoryStream ms = new();
+ //Encapuslate the memory stream in a writer
+ using (Utf8JsonWriter writer = new(ms))
+ {
+ //Write the starting
+ writer.WriteStartObject();
+ //Write the first object name
+ writer.WritePropertyName(initalName);
+ //Write the inital docuemnt to the stream
+ initial.WriteTo(writer);
+ //Write the second object property
+ writer.WritePropertyName(secondName);
+ //Write the merging document to the stream
+ other.WriteTo(writer);
+ //End the parent element
+ writer.WriteEndObject();
+ }
+ //rewind the buffer
+ _ = ms.Seek(0, System.IO.SeekOrigin.Begin);
+ //Parse the stream into the new document and return it
+ return JsonDocument.Parse(ms);
+ }
+
+ /// <summary>
+ /// Parses a number value into a <see cref="TimeSpan"/> of the specified time
+ /// </summary>
+ /// <param name="el"></param>
+ /// <param name="type">The <see cref="TimeParseType"/> the value represents</param>
+ /// <returns>The <see cref="TimeSpan"/> of the value</returns>
+ /// <exception cref="FormatException"></exception>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ public static TimeSpan GetTimeSpan(this in JsonElement el, TimeParseType type)
+ {
+ return type switch
+ {
+ TimeParseType.Milliseconds => TimeSpan.FromMilliseconds(el.GetDouble()),
+ TimeParseType.Seconds => TimeSpan.FromSeconds(el.GetDouble()),
+ TimeParseType.Minutes => TimeSpan.FromMinutes(el.GetDouble()),
+ TimeParseType.Hours => TimeSpan.FromHours(el.GetDouble()),
+ TimeParseType.Days => TimeSpan.FromDays(el.GetDouble()),
+ TimeParseType.Ticks => TimeSpan.FromTicks(el.GetInt64()),
+ _ => throw new NotSupportedException(),
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Extensions/MemoryExtensions.cs b/lib/Utils/src/Extensions/MemoryExtensions.cs
new file mode 100644
index 0000000..c8ee5ef
--- /dev/null
+++ b/lib/Utils/src/Extensions/MemoryExtensions.cs
@@ -0,0 +1,769 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: MemoryExtensions.cs
+*
+* MemoryExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Text;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+
+namespace VNLib.Utils.Extensions
+{
+ using Utils.Memory;
+ using VNLib.Utils.Resources;
+
+ /// <summary>
+ /// Provides memory based extensions to .NET and VNLib memory abstractions
+ /// </summary>
+ public static class MemoryExtensions
+ {
+ /// <summary>
+ /// Rents a new array and stores it as a resource within an <see cref="OpenResourceHandle{T}"/> to return the
+ /// array when work is completed
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="pool"></param>
+ /// <param name="size">The minimum size array to allocate</param>
+ /// <param name="zero">Should elements from 0 to size be set to default(T)</param>
+ /// <returns>A new <see cref="OpenResourceHandle{T}"/> encapsulating the rented array</returns>
+ public static UnsafeMemoryHandle<T> Lease<T>(this ArrayPool<T> pool, int size, bool zero = false) where T: unmanaged
+ {
+ //Pool buffer handles are considered "safe" so im reusing code for now
+ return new(pool, size, zero);
+ }
+
+ /// <summary>
+ /// Retreives a buffer that is at least the reqested length, and clears the array from 0-size.
+ /// <br></br>
+ /// The array may be larger than the requested size, and the entire buffer is zeroed
+ /// </summary>
+ /// <param name="pool"></param>
+ /// <param name="size">The minimum length of the array</param>
+ /// <param name="zero">True if contents should be zeroed</param>
+ /// <returns>The zeroed array</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static T[] Rent<T>(this ArrayPool<T> pool, int size, bool zero)
+ {
+ //Rent the array
+ T[] arr = pool.Rent(size);
+ //If zero flag is set, zero only the used section
+ if (zero)
+ {
+ Array.Fill(arr, default);
+ }
+ return arr;
+ }
+
+ /// <summary>
+ /// Copies the characters within the memory handle to a <see cref="string"/>
+ /// </summary>
+ /// <returns>The string representation of the buffer</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string ToString<T>(this T charBuffer) where T: IMemoryHandle<char>
+ {
+ return charBuffer.Span.ToString();
+ }
+
+ /// <summary>
+ /// Wraps the <see cref="MemoryHandle{T}"/> instance in System.Buffers.MemoryManager
+ /// wrapper to provide <see cref="Memory{T}"/> buffers from umanaged handles.
+ /// </summary>
+ /// <typeparam name="T">The unmanaged data type</typeparam>
+ /// <param name="handle"></param>
+ /// <param name="ownsHandle">
+ /// A value that indicates if the new <see cref="MemoryManager{T}"/> owns the handle.
+ /// When <c>true</c>, the new <see cref="MemoryManager{T}"/> maintains the lifetime of the handle.
+ /// </param>
+ /// <returns>The <see cref="MemoryManager{T}"/> wrapper</returns>
+ /// <remarks>NOTE: This wrapper now manages the lifetime of the current handle</remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryManager<T> ToMemoryManager<T>(this MemoryHandle<T> handle, bool ownsHandle = true) where T : unmanaged
+ {
+ _ = handle ?? throw new ArgumentNullException(nameof(handle));
+ return new SysBufferMemoryManager<T>(handle, ownsHandle);
+ }
+
+ /// <summary>
+ /// Wraps the <see cref="VnTempBuffer{T}"/> instance in System.Buffers.MemoryManager
+ /// wrapper to provide <see cref="Memory{T}"/> buffers from umanaged handles.
+ /// </summary>
+ /// <typeparam name="T">The unmanaged data type</typeparam>
+ /// <param name="handle"></param>
+ /// <param name="ownsHandle">
+ /// A value that indicates if the new <see cref="MemoryManager{T}"/> owns the handle.
+ /// When <c>true</c>, the new <see cref="MemoryManager{T}"/> maintains the lifetime of the handle.
+ /// </param>
+ /// <returns>The <see cref="MemoryManager{T}"/> wrapper</returns>
+ /// <remarks>NOTE: This wrapper now manages the lifetime of the current handle</remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryManager<T> ToMemoryManager<T>(this VnTempBuffer<T> handle, bool ownsHandle = true) where T : unmanaged
+ {
+ _ = handle ?? throw new ArgumentNullException(nameof(handle));
+ return new SysBufferMemoryManager<T>(handle, ownsHandle);
+ }
+
+ /// <summary>
+ /// Allows direct allocation of a fixed size <see cref="MemoryManager{T}"/> from a <see cref="PrivateHeap"/> instance
+ /// of the specified number of elements
+ /// </summary>
+ /// <typeparam name="T">The unmanaged data type</typeparam>
+ /// <param name="heap"></param>
+ /// <param name="size">The number of elements to allocate on the heap</param>
+ /// <param name="zero">Optionally zeros conents of the block when allocated</param>
+ /// <returns>The <see cref="MemoryManager{T}"/> wrapper around the block of memory</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryManager<T> DirectAlloc<T>(this IUnmangedHeap heap, ulong size, bool zero = false) where T : unmanaged
+ {
+ return new SysBufferMemoryManager<T>(heap, size, zero);
+ }
+
+ /// <summary>
+ /// Allows direct allocation of a fixed size <see cref="MemoryManager{T}"/> from a <see cref="PrivateHeap"/> instance
+ /// of the specified number of elements
+ /// </summary>
+ /// <typeparam name="T">The unmanaged data type</typeparam>
+ /// <param name="heap"></param>
+ /// <param name="size">The number of elements to allocate on the heap</param>
+ /// <param name="zero">Optionally zeros conents of the block when allocated</param>
+ /// <returns>The <see cref="MemoryManager{T}"/> wrapper around the block of memory</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryManager<T> DirectAlloc<T>(this IUnmangedHeap heap, long size, bool zero = false) where T : unmanaged
+ {
+ return size < 0 ? throw new ArgumentOutOfRangeException(nameof(size)) : DirectAlloc<T>(heap, (ulong)size, zero);
+ }
+ /// <summary>
+ /// Gets an offset pointer from the base postion to the number of bytes specified. Performs bounds checks
+ /// </summary>
+ /// <param name="memory"></param>
+ /// <param name="elements">Number of elements of type to offset</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ /// <returns><typeparamref name="T"/> pointer to the memory offset specified</returns>
+ /// [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe T* GetOffset<T>(this MemoryHandle<T> memory, long elements) where T : unmanaged
+ {
+ return elements < 0 ? throw new ArgumentOutOfRangeException(nameof(elements)) : memory.GetOffset((ulong)elements);
+ }
+ /// <summary>
+ /// Resizes the current handle on the heap
+ /// </summary>
+ /// <param name="memory"></param>
+ /// <param name="elements">Positive number of elemnts the current handle should referrence</param>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Resize<T>(this MemoryHandle<T> memory, long elements) where T : unmanaged
+ {
+ if (elements < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(elements));
+ }
+ memory.Resize((ulong)elements);
+ }
+
+ /// <summary>
+ /// Resizes the target handle only if the handle is smaller than the requested element count
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="handle"></param>
+ /// <param name="count">The number of elements to resize to</param>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ResizeIfSmaller<T>(this MemoryHandle<T> handle, long count) where T : unmanaged
+ {
+ if(count < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+ ResizeIfSmaller(handle, (ulong)count);
+ }
+
+ /// <summary>
+ /// Resizes the target handle only if the handle is smaller than the requested element count
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="handle"></param>
+ /// <param name="count">The number of elements to resize to</param>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ResizeIfSmaller<T>(this MemoryHandle<T> handle, ulong count) where T : unmanaged
+ {
+ //Check handle size
+ if(handle.Length < count)
+ {
+ //handle too small, resize
+ handle.Resize(count);
+ }
+ }
+
+#if TARGET_64_BIT
+ /// <summary>
+ /// Gets a 64bit friendly span offset for the current <see cref="MemoryHandle{T}"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="block"></param>
+ /// <param name="offset">The offset (in elements) from the begining of the block</param>
+ /// <param name="size">The size of the block (in elements)</param>
+ /// <returns>The offset span</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe Span<T> GetOffsetSpan<T>(this MemoryHandle<T> block, ulong offset, int size) where T: unmanaged
+ {
+ _ = block ?? throw new ArgumentNullException(nameof(block));
+ if(size < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(size));
+ }
+ if(size == 0)
+ {
+ return Span<T>.Empty;
+ }
+ //Make sure the offset size is within the size of the block
+ if(offset + (ulong)size <= block.Length)
+ {
+ //Get long offset from the destination handle
+ void* ofPtr = block.GetOffset(offset);
+ return new Span<T>(ofPtr, size);
+ }
+ throw new ArgumentOutOfRangeException(nameof(size));
+ }
+ /// <summary>
+ /// Gets a 64bit friendly span offset for the current <see cref="MemoryHandle{T}"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="block"></param>
+ /// <param name="offset">The offset (in elements) from the begining of the block</param>
+ /// <param name="size">The size of the block (in elements)</param>
+ /// <returns>The offset span</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe Span<T> GetOffsetSpan<T>(this MemoryHandle<T> block, long offset, int size) where T : unmanaged
+ {
+ return offset < 0 ? throw new ArgumentOutOfRangeException(nameof(offset)) : block.GetOffsetSpan<T>((ulong)offset, size);
+ }
+
+
+ /// <summary>
+ /// Gets a <see cref="SubSequence{T}"/> window within the current block
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="block"></param>
+ /// <param name="offset">An offset within the handle</param>
+ /// <param name="size">The size of the window</param>
+ /// <returns>The new <see cref="SubSequence{T}"/> within the block</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static SubSequence<T> GetSubSequence<T>(this MemoryHandle<T> block, ulong offset, int size) where T : unmanaged
+ {
+ return new SubSequence<T>(block, offset, size);
+ }
+#else
+
+ /// <summary>
+ /// Gets a <see cref="SubSequence{T}"/> window within the current block
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="block"></param>
+ /// <param name="offset">An offset within the handle</param>
+ /// <param name="size">The size of the window</param>
+ /// <returns>The new <see cref="SubSequence{T}"/> within the block</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static SubSequence<T> GetSubSequence<T>(this MemoryHandle<T> block, int offset, int size) where T : unmanaged
+ {
+ return new SubSequence<T>(block, offset, size);
+ }
+
+ /// <summary>
+ /// Gets a 64bit friendly span offset for the current <see cref="MemoryHandle{T}"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="block"></param>
+ /// <param name="offset">The offset (in elements) from the begining of the block</param>
+ /// <param name="size">The size of the block (in elements)</param>
+ /// <returns>The offset span</returns>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe Span<T> GetOffsetSpan<T>(this MemoryHandle<T> block, long offset, int size) where T : unmanaged
+ {
+ //TODO fix 32bit/64 bit, this is a safe lazy workaround
+ return block.Span.Slice(checked((int) offset), size);
+ }
+#endif
+
+ /// <summary>
+ /// Wraps the current instance with a <see cref="MemoryPool{T}"/> wrapper
+ /// to allow System.Memory buffer rentals.
+ /// </summary>
+ /// <typeparam name="T">The unmanged data type to provide allocations from</typeparam>
+ /// <returns>The new <see cref="MemoryPool{T}"/> heap wrapper.</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryPool<T> ToPool<T>(this IUnmangedHeap heap) where T : unmanaged
+ {
+ return new PrivateBuffersMemoryPool<T>(heap);
+ }
+
+ /// <summary>
+ /// Allocates a structure of the specified type on the current unmanged heap and zero's its memory
+ /// </summary>
+ /// <typeparam name="T">The structure type</typeparam>
+ /// <param name="heap"></param>
+ /// <returns>A pointer to the structure ready for use.</returns>
+ /// <remarks>Allocations must be freed with <see cref="StructFree{T}(IUnmangedHeap, T*)"/></remarks>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe T* StructAlloc<T>(this IUnmangedHeap heap) where T : unmanaged
+ {
+ //Allocate the struct on the heap and zero memory it points to
+ IntPtr handle = heap.Alloc(1, (uint)sizeof(T), true);
+ //returns the handle
+ return (T*)handle;
+ }
+ /// <summary>
+ /// Frees a structure at the specified address from the this heap.
+ /// This must be the same heap the structure was allocated from
+ /// </summary>
+ /// <typeparam name="T">The structure type</typeparam>
+ /// <param name="heap"></param>
+ /// <param name="structPtr">A pointer to the structure</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe void StructFree<T>(this IUnmangedHeap heap, T* structPtr) where T : unmanaged
+ {
+ IntPtr block = new(structPtr);
+ //Free block from heap
+ heap.Free(ref block);
+ //Clear ref
+ *structPtr = default;
+ }
+ /// <summary>
+ /// Allocates a block of unmanaged memory of the number of elements to store of an unmanged type
+ /// </summary>
+ /// <typeparam name="T">Unmanaged data type to create a block of</typeparam>
+ /// <param name="heap"></param>
+ /// <param name="elements">The size of the block (number of elements)</param>
+ /// <param name="zero">A flag that zeros the allocated block before returned</param>
+ /// <returns>The unmanaged <see cref="MemoryHandle{T}"/></returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe MemoryHandle<T> Alloc<T>(this IUnmangedHeap heap, ulong elements, bool zero = false) where T : unmanaged
+ {
+ //Minimum of one element
+ elements = Math.Max(elements, 1);
+ //Get element size
+ uint elementSize = (uint)sizeof(T);
+ //If zero flag is set then specify zeroing memory
+ IntPtr block = heap.Alloc(elements, elementSize, zero);
+ //Return handle wrapper
+ return new MemoryHandle<T>(heap, block, elements, zero);
+ }
+ /// <summary>
+ /// Allocates a block of unmanaged memory of the number of elements to store of an unmanged type
+ /// </summary>
+ /// <typeparam name="T">Unmanaged data type to create a block of</typeparam>
+ /// <param name="heap"></param>
+ /// <param name="elements">The size of the block (number of elements)</param>
+ /// <param name="zero">A flag that zeros the allocated block before returned</param>
+ /// <returns>The unmanaged <see cref="MemoryHandle{T}"/></returns>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryHandle<T> Alloc<T>(this IUnmangedHeap heap, long elements, bool zero = false) where T : unmanaged
+ {
+ return elements < 0 ? throw new ArgumentOutOfRangeException(nameof(elements)) : Alloc<T>(heap, (ulong)elements, zero);
+ }
+ /// <summary>
+ /// Allocates a buffer from the current heap and initialzies it by copying the initial data buffer
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="heap"></param>
+ /// <param name="initialData">The initial data to set the buffer to</param>
+ /// <returns>The initalized <see cref="MemoryHandle{T}"/> block</returns>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryHandle<T> AllocAndCopy<T>(this IUnmangedHeap heap, ReadOnlySpan<T> initialData) where T:unmanaged
+ {
+ MemoryHandle<T> handle = heap.Alloc<T>(initialData.Length);
+ Memory.Copy(initialData, handle, 0);
+ return handle;
+ }
+
+ /// <summary>
+ /// Copies data from the input buffer to the current handle and resizes the handle to the
+ /// size of the buffer
+ /// </summary>
+ /// <typeparam name="T">The unamanged value type</typeparam>
+ /// <param name="handle"></param>
+ /// <param name="input">The input buffer to copy data from</param>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteAndResize<T>(this MemoryHandle<T> handle, ReadOnlySpan<T> input) where T: unmanaged
+ {
+ handle.Resize(input.Length);
+ Memory.Copy(input, handle, 0);
+ }
+
+ /// <summary>
+ /// Allocates a block of unamanged memory of the number of elements of an unmanaged type, and
+ /// returns the <see cref="UnsafeMemoryHandle{T}"/> that must be used cautiously
+ /// </summary>
+ /// <typeparam name="T">The unamanged value type</typeparam>
+ /// <param name="heap">The heap to allocate block from</param>
+ /// <param name="elements">The number of elements to allocate</param>
+ /// <param name="zero">A flag to zero the initial contents of the buffer</param>
+ /// <returns>The allocated handle of the specified number of elements</returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe UnsafeMemoryHandle<T> UnsafeAlloc<T>(this IUnmangedHeap heap, int elements, bool zero = false) where T : unmanaged
+ {
+ if (elements < 1)
+ {
+ throw new ArgumentException("Elements must be greater than 0", nameof(elements));
+ }
+ //Minimum of one element
+ elements = Math.Max(elements, 1);
+ //Get element size
+ uint elementSize = (uint)sizeof(T);
+ //If zero flag is set then specify zeroing memory
+ IntPtr block = heap.Alloc((uint)elements, elementSize, zero);
+ //handle wrapper
+ return new (heap, block, elements);
+ }
+
+ #region VnBufferWriter
+
+ /// <summary>
+ /// Formats and appends a value type to the writer with proper endianess
+ /// </summary>
+ /// <param name="buffer"></param>
+ /// <param name="value">The value to format and append to the buffer</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static void Append<T>(this ref ForwardOnlyWriter<byte> buffer, T value) where T: unmanaged
+ {
+ //Calc size of structure and fix te size of the buffer
+ int size = Unsafe.SizeOf<T>();
+ Span<byte> output = buffer.Remaining[..size];
+
+ //Format value and write to buffer
+ MemoryMarshal.Write(output, ref value);
+
+ //If byte order is reversed, reverse elements
+ if (!BitConverter.IsLittleEndian)
+ {
+ output.Reverse();
+ }
+
+ //Update written posiion
+ buffer.Advance(size);
+ }
+
+ /// <summary>
+ /// Formats and appends a value type to the writer with proper endianess
+ /// </summary>
+ /// <param name="buffer"></param>
+ /// <param name="value">The value to format and append to the buffer</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static void Append<T>(this ref ForwardOnlyMemoryWriter<byte> buffer, T value) where T : struct
+ {
+ //Format value and write to buffer
+ int size = Unsafe.SizeOf<T>();
+ Span<byte> output = buffer.Remaining.Span[..size];
+
+ //Format value and write to buffer
+ MemoryMarshal.Write(output, ref value);
+
+ //If byte order is reversed, reverse elements
+ if (BitConverter.IsLittleEndian)
+ {
+ output.Reverse();
+ }
+
+ //Update written posiion
+ buffer.Advance(size);
+ }
+
+ /// <summary>
+ /// Formats and appends the value to end of the buffer
+ /// </summary>
+ /// <param name="buffer"></param>
+ /// <param name="value">The value to format and append to the buffer</param>
+ /// <param name="format">An optional format argument</param>
+ /// <param name="formatProvider"></param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Append<T>(this ref ForwardOnlyWriter<char> buffer, T value, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = default) where T : ISpanFormattable
+ {
+ //Format value and write to buffer
+ if (!value.TryFormat(buffer.Remaining, out int charsWritten, format, formatProvider))
+ {
+ throw new ArgumentOutOfRangeException(nameof(buffer), "The value could not be formatted and appended to the buffer, because there is not enough available space");
+ }
+ //Update written posiion
+ buffer.Advance(charsWritten);
+ }
+
+ /// <summary>
+ /// Formats and appends the value to end of the buffer
+ /// </summary>
+ /// <param name="buffer"></param>
+ /// <param name="value">The value to format and append to the buffer</param>
+ /// <param name="format">An optional format argument</param>
+ /// <param name="formatProvider"></param>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Append<T>(this ref ForwardOnlyMemoryWriter<char> buffer, T value, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = default) where T : ISpanFormattable
+ {
+ //Format value and write to buffer
+ if (!value.TryFormat(buffer.Remaining.Span, out int charsWritten, format, formatProvider))
+ {
+ throw new ArgumentOutOfRangeException(nameof(buffer), "The value could not be formatted and appended to the buffer, because there is not enough available space");
+ }
+ //Update written posiion
+ buffer.Advance(charsWritten);
+ }
+
+
+
+ /// <summary>
+ /// Encodes a set of characters in the input characters span and any characters
+ /// in the internal buffer into a sequence of bytes that are stored in the input
+ /// byte span. A parameter indicates whether to clear the internal state of the
+ /// encoder after the conversion.
+ /// </summary>
+ /// <param name="enc"></param>
+ /// <param name="chars">Character buffer to encode</param>
+ /// <param name="offset">The offset in the char buffer to begin encoding chars from</param>
+ /// <param name="charCount">The number of characers to encode</param>
+ /// <param name="writer">The buffer writer to use</param>
+ /// <param name="flush">true to clear the internal state of the encoder after the conversion; otherwise, false.</param>
+ /// <returns>The actual number of bytes written at the location indicated by the bytes parameter.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetBytes(this Encoder enc, char[] chars, int offset, int charCount, ref ForwardOnlyWriter<byte> writer, bool flush)
+ {
+ return GetBytes(enc, chars.AsSpan(offset, charCount), ref writer, flush);
+ }
+ /// <summary>
+ /// Encodes a set of characters in the input characters span and any characters
+ /// in the internal buffer into a sequence of bytes that are stored in the input
+ /// byte span. A parameter indicates whether to clear the internal state of the
+ /// encoder after the conversion.
+ /// </summary>
+ /// <param name="enc"></param>
+ /// <param name="chars">The character buffer to encode</param>
+ /// <param name="writer">The buffer writer to use</param>
+ /// <param name="flush">true to clear the internal state of the encoder after the conversion; otherwise, false.</param>
+ /// <returns>The actual number of bytes written at the location indicated by the bytes parameter.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetBytes(this Encoder enc, ReadOnlySpan<char> chars, ref ForwardOnlyWriter<byte> writer, bool flush)
+ {
+ //Encode the characters
+ int written = enc.GetBytes(chars, writer.Remaining, flush);
+ //Update the writer position
+ writer.Advance(written);
+ return written;
+ }
+ /// <summary>
+ /// Encodes a set of characters in the input characters span and any characters
+ /// in the internal buffer into a sequence of bytes that are stored in the input
+ /// byte span.
+ /// </summary>
+ /// <param name="encoding"></param>
+ /// <param name="chars">The character buffer to encode</param>
+ /// <param name="writer">The buffer writer to use</param>
+ /// <returns>The actual number of bytes written at the location indicated by the bytes parameter.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static int GetBytes(this Encoding encoding, ReadOnlySpan<char> chars, ref ForwardOnlyWriter<byte> writer)
+ {
+ //Encode the characters
+ int written = encoding.GetBytes(chars, writer.Remaining);
+ //Update the writer position
+ writer.Advance(written);
+ return written;
+ }
+ /// <summary>
+ /// Decodes a character buffer in the input characters span and any characters
+ /// in the internal buffer into a sequence of bytes that are stored in the input
+ /// byte span.
+ /// </summary>
+ /// <param name="encoding"></param>
+ /// <param name="bytes">The binary buffer to decode</param>
+ /// <param name="writer">The buffer writer to use</param>
+ /// <returns>The actual number of *characters* written at the location indicated by the chars parameter.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static int GetChars(this Encoding encoding, ReadOnlySpan<byte> bytes, ref ForwardOnlyWriter<char> writer)
+ {
+ int charCount = encoding.GetCharCount(bytes);
+ //Encode the characters
+ _ = encoding.GetChars(bytes, writer.Remaining);
+ //Update the writer position
+ writer.Advance(charCount);
+ return charCount;
+ }
+
+ /// <summary>
+ /// Converts the buffer data to a <see cref="PrivateString"/>
+ /// </summary>
+ /// <returns>A <see cref="PrivateString"/> instance that owns the underlying string memory</returns>
+ public static PrivateString ToPrivate(this ref ForwardOnlyWriter<char> buffer) => new(buffer.ToString(), true);
+ /// <summary>
+ /// Gets a <see cref="Span{T}"/> over the modified section of the internal buffer
+ /// </summary>
+ /// <returns>A <see cref="Span{T}"/> over the modified data</returns>
+ public static Span<T> AsSpan<T>(this ref ForwardOnlyWriter<T> buffer) => buffer.Buffer[..buffer.Written];
+
+
+ #endregion
+
+ /// <summary>
+ /// Slices the current array by the specified starting offset to the end
+ /// of the array
+ /// </summary>
+ /// <typeparam name="T">The array type</typeparam>
+ /// <param name="arr"></param>
+ /// <param name="start">The start offset of the new array slice</param>
+ /// <returns>The sliced array</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static T[] Slice<T>(this T[] arr, int start)
+ {
+ if(start < 0 || start > arr.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(start));
+ }
+ Range sliceRange = new(start, arr.Length - start);
+ return RuntimeHelpers.GetSubArray(arr, sliceRange);
+ }
+ /// <summary>
+ /// Slices the current array by the specified starting offset to including the
+ /// speciifed number of items
+ /// </summary>
+ /// <typeparam name="T">The array type</typeparam>
+ /// <param name="arr"></param>
+ /// <param name="start">The start offset of the new array slice</param>
+ /// <param name="count">The size of the new array</param>
+ /// <returns>The sliced array</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static T[] Slice<T>(this T[] arr, int start, int count)
+ {
+ if(start < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(start));
+ }
+ if(count < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+ if(start + count >= arr.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+ if(count == 0)
+ {
+ return Array.Empty<T>();
+ }
+ //Calc the slice range
+ Range sliceRange = new(start, start + count);
+ return RuntimeHelpers.GetSubArray(arr, sliceRange);
+ }
+
+ /// <summary>
+ /// Creates a new sub-sequence over the target handle. (allows for convient sub span)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="handle"></param>
+ /// <param name="start">Intial offset into the handle</param>
+ /// <returns>The sub-sequence of the current handle</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span<T> AsSpan<T>(this IMemoryHandle<T> handle, int start) => handle.Span[start..];
+
+ /// <summary>
+ /// Creates a new sub-sequence over the target handle. (allows for convient sub span)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="handle"></param>
+ /// <param name="start">Intial offset into the handle</param>
+ /// <param name="count">The number of elements within the new sequence</param>
+ /// <returns>The sub-sequence of the current handle</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span<T> AsSpan<T>(this IMemoryHandle<T> handle, int start, int count) => handle.Span.Slice(start, count);
+
+ /// <summary>
+ /// Creates a new sub-sequence over the target handle. (allows for convient sub span)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="handle"></param>
+ /// <param name="start">Intial offset into the handle</param>
+ /// <returns>The sub-sequence of the current handle</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span<T> AsSpan<T>(this in UnsafeMemoryHandle<T> handle, int start) where T: unmanaged => handle.Span[start..];
+
+ /// <summary>
+ /// Creates a new sub-sequence over the target handle. (allows for convient sub span)
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="handle"></param>
+ /// <param name="start">Intial offset into the handle</param>
+ /// <param name="count">The number of elements within the new sequence</param>
+ /// <returns>The sub-sequence of the current handle</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span<T> AsSpan<T>(this in UnsafeMemoryHandle<T> handle, int start, int count) where T : unmanaged => handle.Span.Slice(start, count);
+
+ /// <summary>
+ /// Raises an <see cref="ObjectDisposedException"/> if the current handle
+ /// has been disposed or set as invalid
+ /// </summary>
+ /// <param name="handle"></param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ThrowIfClosed(this SafeHandle handle)
+ {
+ if (handle.IsClosed || handle.IsInvalid)
+ {
+ throw new ObjectDisposedException(handle.GetType().Name);
+ }
+ }
+ }
+}
diff --git a/lib/Utils/src/Extensions/MutexReleaser.cs b/lib/Utils/src/Extensions/MutexReleaser.cs
new file mode 100644
index 0000000..84dd60f
--- /dev/null
+++ b/lib/Utils/src/Extensions/MutexReleaser.cs
@@ -0,0 +1,62 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: MutexReleaser.cs
+*
+* MutexReleaser.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+
+namespace VNLib.Utils.Extensions
+{
+ /// <summary>
+ /// Represents a releaser handle for a <see cref="Mutex"/>
+ /// that has been entered and will be released. Best if used
+ /// within a using() statment
+ /// </summary>
+ public readonly struct MutexReleaser : IDisposable, IEquatable<MutexReleaser>
+ {
+ private readonly Mutex _mutext;
+ internal MutexReleaser(Mutex mutex) => _mutext = mutex;
+ /// <summary>
+ /// Releases the held System.Threading.Mutex once.
+ /// </summary>
+ public readonly void Dispose() => _mutext.ReleaseMutex();
+ /// <summary>
+ /// Releases the held System.Threading.Mutex once.
+ /// </summary>
+ public readonly void ReleaseMutext() => _mutext.ReleaseMutex();
+
+ ///<inheritdoc/>
+ public bool Equals(MutexReleaser other) => _mutext.Equals(other._mutext);
+
+ ///<inheritdoc/>
+ public override bool Equals(object? obj) => obj is MutexReleaser releaser && Equals(releaser);
+
+ ///<inheritdoc/>
+ public override int GetHashCode() => _mutext.GetHashCode();
+
+ ///<inheritdoc/>
+ public static bool operator ==(MutexReleaser left, MutexReleaser right) => left.Equals(right);
+ ///<inheritdoc/>
+ public static bool operator !=(MutexReleaser left, MutexReleaser right) => !(left == right);
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Extensions/SafeLibraryExtensions.cs b/lib/Utils/src/Extensions/SafeLibraryExtensions.cs
new file mode 100644
index 0000000..8866059
--- /dev/null
+++ b/lib/Utils/src/Extensions/SafeLibraryExtensions.cs
@@ -0,0 +1,103 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: SafeLibraryExtensions.cs
+*
+* SafeLibraryExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Reflection;
+
+using VNLib.Utils.Native;
+
+namespace VNLib.Utils.Extensions
+{
+ /// <summary>
+ /// When applied to a delegate, specifies the name of the native method to load
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Delegate)]
+ public sealed class SafeMethodNameAttribute : Attribute
+ {
+ /// <summary>
+ /// Creates a new <see cref="SafeMethodNameAttribute"/>
+ /// </summary>
+ /// <param name="MethodName">The name of the native method</param>
+ public SafeMethodNameAttribute(string MethodName) => this.MethodName = MethodName;
+ /// <summary>
+ /// Creates a new <see cref="SafeMethodNameAttribute"/>, that uses the
+ /// delegate name as the native method name
+ /// </summary>
+ public SafeMethodNameAttribute() => MethodName = null;
+ /// <summary>
+ /// The name of the native method
+ /// </summary>
+ public string? MethodName { get; }
+ }
+
+
+ /// <summary>
+ /// Contains native library extension methods
+ /// </summary>
+ public static class SafeLibraryExtensions
+ {
+ const string _missMemberExceptionMessage = $"The delegate type is missing the required {nameof(SafeMethodNameAttribute)} to designate the native method to load";
+
+ /// <summary>
+ /// Loads a native method from the current <see cref="SafeLibraryHandle"/>
+ /// that has a <see cref="SafeMethodNameAttribute"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="library"></param>
+ /// <returns></returns>
+ /// <exception cref="MissingMemberException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static SafeMethodHandle<T> GetMethod<T>(this SafeLibraryHandle library) where T : Delegate
+ {
+ Type t = typeof(T);
+ //Get the method name attribute
+ SafeMethodNameAttribute? attr = t.GetCustomAttribute<SafeMethodNameAttribute>();
+ _ = attr ?? throw new MissingMemberException(_missMemberExceptionMessage);
+ return library.GetMethod<T>(attr.MethodName ?? t.Name);
+ }
+ /// <summary>
+ /// Loads a native method from the current <see cref="SafeLibraryHandle"/>
+ /// that has a <see cref="SafeMethodNameAttribute"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="library"></param>
+ /// <returns></returns>
+ /// <exception cref="MissingMemberException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ /// <remarks>
+ /// The libraries handle count is left unmodified
+ /// </remarks>
+ public static T DangerousGetMethod<T>(this SafeLibraryHandle library) where T: Delegate
+ {
+ Type t = typeof(T);
+ //Get the method name attribute
+ SafeMethodNameAttribute? attr = t.GetCustomAttribute<SafeMethodNameAttribute>();
+ return string.IsNullOrWhiteSpace(attr?.MethodName)
+ ? throw new MissingMemberException(_missMemberExceptionMessage)
+ : library.DangerousGetMethod<T>(attr.MethodName);
+ }
+ }
+}
diff --git a/lib/Utils/src/Extensions/SemSlimReleaser.cs b/lib/Utils/src/Extensions/SemSlimReleaser.cs
new file mode 100644
index 0000000..c3be4f8
--- /dev/null
+++ b/lib/Utils/src/Extensions/SemSlimReleaser.cs
@@ -0,0 +1,62 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: SemSlimReleaser.cs
+*
+* SemSlimReleaser.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+
+namespace VNLib.Utils.Extensions
+{
+ /// <summary>
+ /// Represents a releaser handle for a <see cref="SemaphoreSlim"/>
+ /// that has been entered and will be released. Best if used
+ /// within a using() statment
+ /// </summary>
+ public readonly struct SemSlimReleaser : IDisposable, IEquatable<SemSlimReleaser>
+ {
+ private readonly SemaphoreSlim _semaphore;
+ internal SemSlimReleaser(SemaphoreSlim semaphore) => _semaphore = semaphore;
+ /// <summary>
+ /// Releases the System.Threading.SemaphoreSlim object once.
+ /// </summary>
+ public readonly void Dispose() => _semaphore.Release();
+ /// <summary>
+ /// Releases the System.Threading.SemaphoreSlim object once.
+ /// </summary>
+ /// <returns>The previous count of the <see cref="SemaphoreSlim"/></returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="SemaphoreFullException"></exception>
+ public readonly int Release() => _semaphore.Release();
+
+ ///<inheritdoc/>
+ public override bool Equals(object obj) => obj is SemSlimReleaser ssr && Equals(ssr);
+ ///<inheritdoc/>
+ public override int GetHashCode() => _semaphore.GetHashCode();
+ ///<inheritdoc/>
+ public static bool operator ==(SemSlimReleaser left, SemSlimReleaser right) => left.Equals(right);
+ ///<inheritdoc/>
+ public static bool operator !=(SemSlimReleaser left, SemSlimReleaser right) => !(left == right);
+ ///<inheritdoc/>
+ public bool Equals(SemSlimReleaser other) => _semaphore == other._semaphore;
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Extensions/StringExtensions.cs b/lib/Utils/src/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..09d6517
--- /dev/null
+++ b/lib/Utils/src/Extensions/StringExtensions.cs
@@ -0,0 +1,481 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: StringExtensions.cs
+*
+* StringExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using VNLib.Utils.Memory;
+
+namespace VNLib.Utils.Extensions
+{
+ public delegate void StatelessSpanAction(ReadOnlySpan<char> line);
+
+ /// <summary>
+ /// Extention methods for string (character buffer)
+ /// </summary>
+ public static class StringExtensions
+ {
+ /// <summary>
+ /// Split a string based on split value and insert into the specified list
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="splitter">The value to split the string on</param>
+ /// <param name="output">The list to output data to</param>
+ /// <param name="options">String split options</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Split<T>(this string value, string splitter, T output, StringSplitOptions options) where T : ICollection<string>
+ {
+ Split(value, splitter.AsSpan(), output, options);
+ }
+ /// <summary>
+ /// Split a string based on split value and insert into the specified list
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="splitter">The value to split the string on</param>
+ /// <param name="output">The list to output data to</param>
+ /// <param name="options">String split options</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Split<T>(this string value, char splitter, T output, StringSplitOptions options) where T: ICollection<string>
+ {
+ //Create span from char pointer
+ ReadOnlySpan<char> cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1);
+ //Call the split function on the span
+ Split(value, cs, output, options);
+ }
+ /// <summary>
+ /// Split a string based on split value and insert into the specified list
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="splitter">The value to split the string on</param>
+ /// <param name="output">The list to output data to</param>
+ /// <param name="options">String split options</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Split<T>(this string value, ReadOnlySpan<char> splitter, T output, StringSplitOptions options) where T : ICollection<string>
+ {
+ Split(value.AsSpan(), splitter, output, options);
+ }
+ /// <summary>
+ /// Split a string based on split value and insert into the specified list
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="splitter">The value to split the string on</param>
+ /// <param name="output">The list to output data to</param>
+ /// <param name="options">String split options</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Split<T>(this in ReadOnlySpan<char> value, char splitter, T output, StringSplitOptions options) where T : ICollection<string>
+ {
+ //Create span from char pointer
+ ReadOnlySpan<char> cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1);
+ //Call the split function on the span
+ Split(in value, cs, output, options);
+ }
+ /// <summary>
+ /// Split a <see cref="ReadOnlySpan{T}"/> based on split value and insert into the specified list
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="splitter">The value to split the string on</param>
+ /// <param name="output">The list to output data to</param>
+ /// <param name="options">String split options</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Split<T>(this in ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, T output, StringSplitOptions options) where T : ICollection<string>
+ {
+ //Create a local function that adds the split strings to the list
+ static void SplitFound(ReadOnlySpan<char> split, T output) => output.Add(split.ToString());
+ //Invoke the split function with the local callback method
+ Split(in value, splitter, options, SplitFound, output);
+ }
+ /// <summary>
+ /// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="splitter">The sequence to split the string on</param>
+ /// <param name="options">String split options</param>
+ /// <param name="splitCb">The action to invoke when a split segment has been found</param>
+ /// <param name="state">The state to pass to the callback handler</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static void Split<T>(this in ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, StringSplitOptions options, ReadOnlySpanAction<char, T> splitCb, T state)
+ {
+ _ = splitCb ?? throw new ArgumentNullException(nameof(splitCb));
+ //Get span over string
+ ForwardOnlyReader<char> reader = new(value);
+ //No string options
+ if (options == 0)
+ {
+ do
+ {
+ //Find index of the splitter
+ int start = reader.Window.IndexOf(splitter);
+ //guard
+ if (start == -1)
+ {
+ break;
+ }
+ //Trim and add it regardless of length
+ splitCb(reader.Window[..start], state);
+ //shift window
+ reader.Advance(start + splitter.Length);
+ } while (true);
+ //Trim remaining and add it regardless of length
+ splitCb(reader.Window, state);
+ }
+ //Trim but do not remove empties
+ else if ((options & StringSplitOptions.RemoveEmptyEntries) == 0)
+ {
+ do
+ {
+ //Find index of the splitter
+ int start = reader.Window.IndexOf(splitter);
+ //guard
+ if (start == -1)
+ {
+ break;
+ }
+ //Trim and add it regardless of length
+ splitCb(reader.Window[..start].Trim(), state);
+ //shift window
+ reader.Advance(start + splitter.Length);
+ } while (true);
+ //Trim remaining and add it regardless of length
+ splitCb(reader.Window.Trim(), state);
+ }
+ //Remove empty entires but do not trim them
+ else if ((options & StringSplitOptions.TrimEntries) == 0)
+ {
+ //Get data before splitter and trim it
+ ReadOnlySpan<char> data;
+ do
+ {
+ //Find index of the splitter
+ int start = reader.Window.IndexOf(splitter);
+ //guard
+ if (start == -1)
+ {
+ break;
+ }
+ //Get data before splitter and trim it
+ data = reader.Window[..start];
+ //If its not empty, then add it to the list
+ if (!data.IsEmpty)
+ {
+ splitCb(data, state);
+ }
+ //shift window
+ reader.Advance(start + splitter.Length);
+ } while (true);
+ //Add if not empty
+ if (reader.WindowSize > 0)
+ {
+ splitCb(reader.Window, state);
+ }
+ }
+ //Must mean remove and trim
+ else
+ {
+ //Get data before splitter and trim it
+ ReadOnlySpan<char> data;
+ do
+ {
+ //Find index of the splitter
+ int start = reader.Window.IndexOf(splitter);
+ //guard
+ if (start == -1)
+ {
+ break;
+ }
+ //Get data before splitter and trim it
+ data = reader.Window[..start].Trim();
+ //If its not empty, then add it to the list
+ if (!data.IsEmpty)
+ {
+ splitCb(data, state);
+ }
+ //shift window
+ reader.Advance(start + splitter.Length);
+ } while (true);
+ //Trim remaining
+ data = reader.Window.Trim();
+ //Add if not empty
+ if (!data.IsEmpty)
+ {
+ splitCb(data, state);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="splitter">The character to split the string on</param>
+ /// <param name="options">String split options</param>
+ /// <param name="splitCb">The action to invoke when a split segment has been found</param>
+ /// <param name="state"></param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Split<T>(this in ReadOnlySpan<char> value, char splitter, StringSplitOptions options, ReadOnlySpanAction<char, T> splitCb, T state)
+ {
+ //Alloc a span for char
+ ReadOnlySpan<char> cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1);
+ //Call the split function on the span
+ Split(in value, cs, options, splitCb, state);
+ }
+ /// <summary>
+ /// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="splitter">The sequence to split the string on</param>
+ /// <param name="options">String split options</param>
+ /// <param name="splitCb">The action to invoke when a split segment has been found</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Split(this in ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, StringSplitOptions options, StatelessSpanAction splitCb)
+ {
+ //Create a SpanSplitDelegate with the non-typed delegate as the state argument
+ static void ssplitcb(ReadOnlySpan<char> param, StatelessSpanAction callback) => callback(param);
+ //Call split with the new callback delegate
+ Split(in value, splitter, options, ssplitcb, splitCb);
+ }
+ /// <summary>
+ /// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler
+ /// </summary>
+ /// <param name="value"></param>
+ /// <param name="splitter">The character to split the string on</param>
+ /// <param name="options">String split options</param>
+ /// <param name="splitCb">The action to invoke when a split segment has been found</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Split(this in ReadOnlySpan<char> value, char splitter, StringSplitOptions options, StatelessSpanAction splitCb)
+ {
+ //Create a SpanSplitDelegate with the non-typed delegate as the state argument
+ static void ssplitcb(ReadOnlySpan<char> param, StatelessSpanAction callback) => callback(param);
+ //Call split with the new callback delegate
+ Split(in value, splitter, options, ssplitcb, splitCb);
+ }
+
+ /// <summary>
+ /// Gets the index of the end of the found sequence
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="search">Sequence to search for within the current sequence</param>
+ /// <returns>the index of the end of the sequenc</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int EndOf(this in ReadOnlySpan<char> data, ReadOnlySpan<char> search)
+ {
+ int index = data.IndexOf(search);
+ return index > -1 ? index + search.Length : -1;
+ }
+ /// <summary>
+ /// Gets the index of the end of the found character
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="search">Character to search for within the current sequence</param>
+ /// <returns>the index of the end of the sequence</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int EndOf(this in ReadOnlySpan<char> data, char search)
+ {
+ int index = data.IndexOf(search);
+ return index > -1 ? index + 1 : -1;
+ }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int IndexOf(this in Memory<byte> data, byte search) => data.Span.IndexOf(search);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int IndexOf(this in Memory<byte> data, ReadOnlySpan<byte> search) => data.Span.IndexOf(search);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int IndexOf(this in Memory<byte> data, ReadOnlyMemory<byte> search) => IndexOf(data, search.Span);
+
+ /// <summary>
+ /// Slices the current span from the begining of the segment to the first occurrance of the specified character.
+ /// If the character is not found, the entire segment is returned
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="search">The delimiting character</param>
+ /// <returns>The segment of data before the search character, or the entire segment if not found</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySpan<char> SliceBeforeParam(this in ReadOnlySpan<char> data, char search)
+ {
+ //Find the index of the specified data
+ int index = data.IndexOf(search);
+ //Return the slice of data before the index, or an empty span if it was not found
+ return index > -1 ? data[..index] : data;
+ }
+ /// <summary>
+ /// Slices the current span from the begining of the segment to the first occurrance of the specified character sequence.
+ /// If the character sequence is not found, the entire segment is returned
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="search">The delimiting character sequence</param>
+ /// <returns>The segment of data before the search character, or the entire <paramref name="data"/> if the seach sequence is not found</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySpan<char> SliceBeforeParam(this in ReadOnlySpan<char> data, ReadOnlySpan<char> search)
+ {
+ //Find the index of the specified data
+ int index = data.IndexOf(search);
+ //Return the slice of data before the index, or an empty span if it was not found
+ return index > -1 ? data[..index] : data;
+ }
+ /// <summary>
+ /// Gets the remaining segment of data after the specified search character or <see cref="ReadOnlySpan{T}.Empty"/>
+ /// if the search character is not found within the current segment
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="search">The character to search for within the segment</param>
+ /// <returns>The segment of data after the search character or <see cref="ReadOnlySpan{T}.Empty"/> if not found</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySpan<char> SliceAfterParam(this in ReadOnlySpan<char> data, char search)
+ {
+ //Find the index of the specified data
+ int index = EndOf(in data, search);
+ //Return the slice of data after the index, or an empty span if it was not found
+ return index > -1 ? data[index..] : ReadOnlySpan<char>.Empty;
+ }
+ /// <summary>
+ /// Gets the remaining segment of data after the specified search sequence or <see cref="ReadOnlySpan{T}.Empty"/>
+ /// if the search sequence is not found within the current segment
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="search">The sequence to search for within the segment</param>
+ /// <returns>The segment of data after the search sequence or <see cref="ReadOnlySpan{T}.Empty"/> if not found</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySpan<char> SliceAfterParam(this in ReadOnlySpan<char> data, ReadOnlySpan<char> search)
+ {
+ //Find the index of the specified data
+ int index = EndOf(data, search);
+ //Return the slice of data after the index, or an empty span if it was not found
+ return index > -1 ? data[index..] : ReadOnlySpan<char>.Empty;
+ }
+ /// <summary>
+ /// Trims any leading or trailing <c>'\r'|'\n'|' '</c>(whitespace) characters from the segment
+ /// </summary>
+ /// <returns>The trimmed segment</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySpan<char> TrimCRLF(this in ReadOnlySpan<char> data)
+ {
+ int start = 0, end = data.Length;
+ //trim leading \r\n chars
+ while(start < end)
+ {
+ char t = data[start];
+ //If character \r or \n slice it off
+ if (t != '\r' && t != '\n' && t != ' ') {
+ break;
+ }
+ //Shift
+ start++;
+ }
+ //remove trailing crlf characters
+ while (end > start)
+ {
+ char t = data[end - 1];
+ //If character \r or \n slice it off
+ if (t != '\r' && t != '\n' && t != ' ') {
+ break;
+ }
+ end--;
+ }
+ return data[start..end];
+ }
+
+ /// <summary>
+ /// Replaces a character sequence within the buffer
+ /// </summary>
+ /// <param name="buffer">The character buffer to process</param>
+ /// <param name="search">The sequence to search for</param>
+ /// <param name="replace">The sequence to write in the place of the search parameter</param>
+ /// <exception cref="OutOfMemoryException"></exception>
+ public static int Replace(this ref Span<char> buffer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace)
+ {
+ ForwardOnlyWriter<char> writer = new (buffer);
+ writer.Replace(search, replace);
+ return writer.Written;
+ }
+
+ /// <summary>
+ /// Replaces a character sequence within the writer
+ /// </summary>
+ /// <param name="writer"></param>
+ /// <param name="search">The sequence to search for</param>
+ /// <param name="replace">The sequence to write in the place of the search parameter</param>
+ /// <exception cref="OutOfMemoryException"></exception>
+ public static void Replace(this ref ForwardOnlyWriter<char> writer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace)
+ {
+ Span<char> buffer = writer.AsSpan();
+ //If the search and replacment parameters are the same length
+ if (search.Length == replace.Length)
+ {
+ buffer.ReplaceInPlace(search, replace);
+ return;
+ }
+ //Search and replace are not the same length
+ int searchLen = search.Length, start = buffer.IndexOf(search);
+ if(start == -1)
+ {
+ return;
+ }
+ //Replacment might be empty
+ writer.Reset();
+ do
+ {
+ //Append the data before the split character
+ writer.Append(buffer[..start]);
+ //Append the replacment
+ writer.Append(replace);
+ //Shift buffer to the end of the
+ buffer = buffer[(start + searchLen)..];
+ //search for next index
+ start = buffer.IndexOf(search);
+ } while (start > -1);
+ //Write remaining data
+ writer.Append(replace);
+ }
+ /// <summary>
+ /// Replaces very ocurrance of character sequence within a buffer with another sequence of the same length
+ /// </summary>
+ /// <param name="buffer"></param>
+ /// <param name="search">The sequence to search for</param>
+ /// <param name="replace">The sequence to replace the found sequence with</param>
+ /// <exception cref="ArgumentException"></exception>
+ public static void ReplaceInPlace(this Span<char> buffer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace)
+ {
+ if(search.Length != replace.Length)
+ {
+ throw new ArgumentException("Search parameter and replacment parameter must be the same length");
+ }
+ int start = buffer.IndexOf(search);
+ while(start > -1)
+ {
+ //Shift the buffer to the begining of the search parameter
+ buffer = buffer[start..];
+ //Overwite the search parameter
+ replace.CopyTo(buffer);
+ //Search for next index of the search character
+ start = buffer.IndexOf(search);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Extensions/ThreadingExtensions.cs b/lib/Utils/src/Extensions/ThreadingExtensions.cs
new file mode 100644
index 0000000..cc9fab9
--- /dev/null
+++ b/lib/Utils/src/Extensions/ThreadingExtensions.cs
@@ -0,0 +1,226 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ThreadingExtensions.cs
+*
+* ThreadingExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using VNLib.Utils.Resources;
+
+namespace VNLib.Utils.Extensions
+{
+
+ /// <summary>
+ /// Provides extension methods to common threading and TPL library operations
+ /// </summary>
+ public static class ThreadingExtensions
+ {
+ /// <summary>
+ /// Allows an <see cref="OpenResourceHandle{TResource}"/> to execute within a scope limited context
+ /// </summary>
+ /// <typeparam name="TResource">The resource type</typeparam>
+ /// <param name="rh"></param>
+ /// <param name="safeCallback">The function body that will execute with controlled access to the resource</param>
+ public static void EnterSafeContext<TResource>(this OpenResourceHandle<TResource> rh, Action<TResource> safeCallback)
+ {
+ using (rh)
+ {
+ safeCallback(rh.Resource);
+ }
+ }
+
+ /// <summary>
+ /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/> while observing a <see cref="CancellationToken"/>
+ /// and getting a releaser handle
+ /// </summary>
+ /// <param name="semaphore"></param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>A releaser handle that may be disposed to release the semaphore</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="OperationCanceledException"></exception>
+ public static async Task<SemSlimReleaser> GetReleaserAsync(this SemaphoreSlim semaphore, CancellationToken cancellationToken = default)
+ {
+ await semaphore.WaitAsync(cancellationToken);
+ return new SemSlimReleaser(semaphore);
+ }
+ /// <summary>
+ /// Asynchronously waits to enter the <see cref="SemaphoreSlim"/> using a 32-bit signed integer to measure the time intervale
+ /// and getting a releaser handle
+ /// </summary>
+ /// <param name="semaphore"></param>
+ /// <param name="timeout">A the maximum amount of time in milliseconds to wait to enter the semaphore</param>
+ /// <returns>A releaser handle that may be disposed to release the semaphore</returns>
+ /// <exception cref="TimeoutException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static async Task<SemSlimReleaser> GetReleaserAsync(this SemaphoreSlim semaphore, int timeout)
+ {
+ if (await semaphore.WaitAsync(timeout))
+ {
+ return new SemSlimReleaser(semaphore);
+ }
+ throw new TimeoutException("Failed to enter the semaphore before the specified timeout period");
+ }
+
+ /// <summary>
+ /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>
+ /// </summary>
+ /// <param name="semaphore"></param>
+ /// <returns>A releaser handler that releases the semaphore when disposed</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static SemSlimReleaser GetReleaser(this SemaphoreSlim semaphore)
+ {
+ semaphore.Wait();
+ return new SemSlimReleaser(semaphore);
+ }
+ /// <summary>
+ /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>
+ /// </summary>
+ /// <param name="semaphore"></param>
+ /// <param name="timeout">A the maximum amount of time in milliseconds to wait to enter the semaphore</param>
+ /// <returns>A releaser handler that releases the semaphore when disposed</returns>
+ /// <exception cref="TimeoutException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static SemSlimReleaser GetReleaser(this SemaphoreSlim semaphore, int timeout)
+ {
+ if (semaphore.Wait(timeout))
+ {
+ return new SemSlimReleaser(semaphore);
+ }
+ throw new TimeoutException("Failed to enter the semaphore before the specified timeout period");
+ }
+
+ /// <summary>
+ /// Blocks the current thread until it can enter the <see cref="Mutex"/>
+ /// </summary>
+ /// <param name="mutex"></param>
+ /// <returns>A releaser handler that releases the semaphore when disposed</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="AbandonedMutexException"></exception>
+ public static MutexReleaser Enter(this Mutex mutex)
+ {
+ mutex.WaitOne();
+ return new MutexReleaser(mutex);
+ }
+ /// <summary>
+ /// Blocks the current thread until it can enter the <see cref="SemaphoreSlim"/>
+ /// </summary>
+ /// <param name="mutex"></param>
+ /// <param name="timeout">A the maximum amount of time in milliseconds to wait to enter the semaphore</param>
+ /// <returns>A releaser handler that releases the semaphore when disposed</returns>
+ /// <exception cref="TimeoutException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static MutexReleaser Enter(this Mutex mutex, int timeout)
+ {
+ if (mutex.WaitOne(timeout))
+ {
+ return new MutexReleaser(mutex);
+ }
+ throw new TimeoutException("Failed to enter the semaphore before the specified timeout period");
+ }
+
+ private static readonly Task<bool> TrueCompleted = Task.FromResult(true);
+ private static readonly Task<bool> FalseCompleted = Task.FromResult(false);
+
+ /// <summary>
+ /// Asynchronously waits for a the <see cref="WaitHandle"/> to receive a signal. This method spins until
+ /// a thread yield will occur, then asynchronously yields.
+ /// </summary>
+ /// <param name="handle"></param>
+ /// <param name="timeoutMs">The timeout interval in milliseconds</param>
+ /// <returns>
+ /// A task that compeletes when the wait handle receives a signal or times-out,
+ /// the result of the awaited task will be <c>true</c> if the signal is received, or
+ /// <c>false</c> if the timeout interval expires
+ /// </returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static Task<bool> WaitAsync(this WaitHandle handle, int timeoutMs = Timeout.Infinite)
+ {
+ _ = handle ?? throw new ArgumentNullException(nameof(handle));
+ //test non-blocking handle state
+ if (handle.WaitOne(0))
+ {
+ return TrueCompleted;
+ }
+ //When timeout is 0, wh will block, return false
+ else if(timeoutMs == 0)
+ {
+ return FalseCompleted;
+ }
+ //Init short lived spinwait
+ SpinWait sw = new();
+ //Spin until yield occurs
+ while (!sw.NextSpinWillYield)
+ {
+ sw.SpinOnce();
+ //Check handle state
+ if (handle.WaitOne(0))
+ {
+ return TrueCompleted;
+ }
+ }
+ //Completion source used to signal the awaiter when the wait handle is signaled
+ TaskCompletionSource<bool> completion = new(TaskCreationOptions.None);
+ //Register wait on threadpool to complete the task source
+ RegisteredWaitHandle registration = ThreadPool.RegisterWaitForSingleObject(handle, TaskCompletionCallback, completion, timeoutMs, true);
+ //Register continuation to cleanup
+ _ = completion.Task.ContinueWith(CleanupContinuation, registration, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default)
+ .ConfigureAwait(false);
+ return completion.Task;
+ }
+
+ private static void CleanupContinuation(Task<bool> task, object? taskCompletion)
+ {
+ RegisteredWaitHandle registration = (taskCompletion as RegisteredWaitHandle)!;
+ registration.Unregister(null);
+ task.Dispose();
+ }
+ private static void TaskCompletionCallback(object? tcsState, bool timedOut)
+ {
+ TaskCompletionSource<bool> completion = (tcsState as TaskCompletionSource<bool>)!;
+ //Set the result of the wait handle timeout
+ _ = completion.TrySetResult(!timedOut);
+ }
+
+
+ /// <summary>
+ /// Registers a callback method that will be called when the token has been cancelled.
+ /// This method waits indefinitely for the token to be cancelled.
+ /// </summary>
+ /// <param name="token"></param>
+ /// <param name="callback">The callback method to invoke when the token has been cancelled</param>
+ /// <returns>A task that may be unobserved, that completes when the token has been cancelled</returns>
+ public static Task RegisterUnobserved(this CancellationToken token, Action callback)
+ {
+ //Call callback when the wait handle is set
+ return token.WaitHandle.WaitAsync()
+ .ContinueWith(static (t, callback) => (callback as Action)!.Invoke(),
+ callback,
+ CancellationToken.None,
+ TaskContinuationOptions.ExecuteSynchronously,
+ TaskScheduler.Default
+ );
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Extensions/TimerExtensions.cs b/lib/Utils/src/Extensions/TimerExtensions.cs
new file mode 100644
index 0000000..37929bf
--- /dev/null
+++ b/lib/Utils/src/Extensions/TimerExtensions.cs
@@ -0,0 +1,71 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: TimerExtensions.cs
+*
+* TimerExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+
+using VNLib.Utils.Resources;
+
+namespace VNLib.Utils.Extensions
+{
+
+ /// <summary>
+ /// Contains extension methods for <see cref="Timer"/>
+ /// </summary>
+ public static class TimerExtensions
+ {
+ /// <summary>
+ /// Attempts to stop the timer
+ /// </summary>
+ /// <returns>True if the timer was successfully modified, false otherwise</returns>
+ public static bool Stop(this Timer timer) => timer.Change(Timeout.Infinite, Timeout.Infinite);
+
+ /// <summary>
+ /// Attempts to stop an active timer and prepare a <see cref="OpenHandle"/> configured to restore the state of the timer to the specified timespan
+ /// </summary>
+ /// <param name="timer"></param>
+ /// <param name="resumeTime"><see cref="TimeSpan"/> representing the amount of time the timer should wait before invoking the callback function</param>
+ /// <returns>A new <see cref="OpenHandle"/> if the timer was stopped successfully that will resume the timer when closed, null otherwise</returns>
+ public static TimerResetHandle? Stop(this Timer timer, TimeSpan resumeTime)
+ {
+ return timer.Change(Timeout.Infinite, Timeout.Infinite) ? new TimerResetHandle(timer, resumeTime) : null;
+ }
+
+ /// <summary>
+ /// Attempts to reset and start a timer
+ /// </summary>
+ /// <param name="timer"></param>
+ /// <param name="wait"><see cref="TimeSpan"/> to wait before the timer event is fired</param>
+ /// <returns>True if the timer was successfully modified</returns>
+ public static bool Restart(this Timer timer, TimeSpan wait) => timer.Change(wait, Timeout.InfiniteTimeSpan);
+
+ /// <summary>
+ /// Attempts to reset and start a timer
+ /// </summary>
+ /// <param name="timer"></param>
+ /// <param name="waitMilliseconds">Time in milliseconds to wait before the timer event is fired</param>
+ /// <returns>True if the timer was successfully modified</returns>
+ public static bool Restart(this Timer timer, int waitMilliseconds) => timer.Change(waitMilliseconds, Timeout.Infinite);
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Extensions/TimerResetHandle.cs b/lib/Utils/src/Extensions/TimerResetHandle.cs
new file mode 100644
index 0000000..57a71e8
--- /dev/null
+++ b/lib/Utils/src/Extensions/TimerResetHandle.cs
@@ -0,0 +1,66 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: TimerExtensions.cs
+*
+* TimerExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+
+namespace VNLib.Utils.Extensions
+{
+ /// <summary>
+ /// A handle that represents a paused timer that may be resumed when the handle is disposed
+ /// or the Resume() method is called
+ /// </summary>
+ public readonly struct TimerResetHandle: IEquatable<TimerResetHandle>, IDisposable
+ {
+ private readonly Timer _timer;
+ private readonly TimeSpan _resumeTime;
+
+ internal TimerResetHandle(Timer timer, TimeSpan resumeTime)
+ {
+ _timer = timer;
+ _resumeTime = resumeTime;
+ }
+
+ /// <summary>
+ /// Resumes the timer to the configured time from the call to Timer.Stop()
+ /// </summary>
+ public void Resume() => _timer.Change(_resumeTime, Timeout.InfiniteTimeSpan);
+ /// <summary>
+ /// Releases any resources held by the Handle, and resumes the timer to
+ /// the configured time from the call to Timer.Stop()
+ /// </summary>
+ public void Dispose() => Resume();
+
+ ///<inheritdoc/>
+ public bool Equals(TimerResetHandle other) => _timer.Equals(other) && _resumeTime == other._resumeTime;
+ ///<inheritdoc/>
+ public override bool Equals(object? obj) => obj is TimerResetHandle trh && Equals(trh);
+ ///<inheritdoc/>
+ public override int GetHashCode() => _timer.GetHashCode() + _resumeTime.GetHashCode();
+ ///<inheritdoc/>
+ public static bool operator ==(TimerResetHandle left, TimerResetHandle right) => left.Equals(right);
+ ///<inheritdoc/>
+ public static bool operator !=(TimerResetHandle left, TimerResetHandle right) => !(left == right);
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Extensions/VnStringExtensions.cs b/lib/Utils/src/Extensions/VnStringExtensions.cs
new file mode 100644
index 0000000..285fc4f
--- /dev/null
+++ b/lib/Utils/src/Extensions/VnStringExtensions.cs
@@ -0,0 +1,418 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: VnStringExtensions.cs
+*
+* VnStringExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+using VNLib.Utils.Memory;
+
+namespace VNLib.Utils.Extensions
+{
+ [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "<Pending>")]
+ public static class VnStringExtensions
+ {
+ /// <summary>
+ /// Derermines if the character exists within the instance
+ /// </summary>
+ /// <param name="str"></param>
+ /// <param name="value">The value to find</param>
+ /// <returns>True if the character exists within the instance</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static bool Contains(this VnString str, char value) => str.AsSpan().Contains(value);
+ /// <summary>
+ /// Derermines if the sequence exists within the instance
+ /// </summary>
+ /// <param name="str"></param>
+ /// <param name="value">The sequence to find</param>
+ /// <param name="stringComparison"></param>
+ /// <returns>True if the character exists within the instance</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+
+ public static bool Contains(this VnString str, ReadOnlySpan<char> value, StringComparison stringComparison) => str.AsSpan().Contains(value, stringComparison);
+
+ /// <summary>
+ /// Searches for the first occurrance of the specified character within the current instance
+ /// </summary>
+ /// <param name="str"></param>
+ /// <param name="value">The character to search for within the instance</param>
+ /// <returns>The 0 based index of the occurance, -1 if the character was not found</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static int IndexOf(this VnString str, char value) => str.IsEmpty ? -1 : str.AsSpan().IndexOf(value);
+ /// <summary>
+ /// Searches for the first occurrance of the specified sequence within the current instance
+ /// </summary>
+ /// <param name="str"></param>
+ /// <param name="search">The sequence to search for</param>
+ /// <returns>The 0 based index of the occurance, -1 if the sequence was not found</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static int IndexOf(this VnString str, ReadOnlySpan<char> search)
+ {
+ //Using spans to avoid memory leaks...
+ ReadOnlySpan<char> self = str.AsSpan();
+ return self.IndexOf(search);
+ }
+ /// <summary>
+ /// Searches for the first occurrance of the specified sequence within the current instance
+ /// </summary>
+ /// <param name="str"></param>
+ /// <param name="search">The sequence to search for</param>
+ /// <param name="comparison">The <see cref="StringComparison"/> type to use in searchr</param>
+ /// <returns>The 0 based index of the occurance, -1 if the sequence was not found</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static int IndexOf(this VnString str, ReadOnlySpan<char> search, StringComparison comparison)
+ {
+ //Using spans to avoid memory leaks...
+ ReadOnlySpan<char> self = str.AsSpan();
+ return self.IndexOf(search, comparison);
+ }
+ /// <summary>
+ /// Searches for the 0 based index of the first occurance of the search parameter after the start index.
+ /// </summary>
+ /// <param name="str"></param>
+ /// <param name="search">The sequence of data to search for</param>
+ /// <param name="start">The lower boundry of the search area</param>
+ /// <returns>The absolute index of the first occurrance within the instance, -1 if the sequency was not found in the windowed segment</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static int IndexOf(this VnString str, ReadOnlySpan<char> search, int start)
+ {
+ if (start < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(start), "Start cannot be less than 0");
+ }
+ //Get shifted window
+ ReadOnlySpan<char> self = str.AsSpan()[start..];
+ //Check indexof
+ int index = self.IndexOf(search);
+ return index > -1 ? index + start : -1;
+ }
+
+ /// <summary>
+ /// Returns the realtive index after the specified sequence within the <see cref="VnString"/> instance
+ /// </summary>
+ /// <param name="str"></param>
+ /// <param name="search">The sequence to search for</param>
+ /// <returns>The index after the found sequence within the string, -1 if the sequence was not found within the instance</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static int EndOf(this VnString str, ReadOnlySpan<char> search)
+ {
+ //Try to get the index of the data
+ int index = IndexOf(str, search);
+ //If the data was found, add the length to get the end of the string
+ return index > -1 ? index + search.Length : -1;
+ }
+
+ /// <summary>
+ /// Allows for trimming whitespace characters in a realtive sequence from
+ /// within a <see cref="VnString"/> buffer defined by the start and end parameters
+ /// and returning the trimmed entry.
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="start">The starting position within the sequence to trim</param>
+ /// <param name="end">The end of the sequence to trim</param>
+ /// <returns>The trimmed <see cref="VnString"/> instance as a child of the original entry</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static VnString AbsoluteTrim(this VnString data, int start, int end)
+ {
+ AbsoluteTrim(data, ref start, ref end);
+ return data[start..end];
+ }
+ /// <summary>
+ /// Finds whitespace characters within the sequence defined between start and end parameters
+ /// and adjusts the specified window to "trim" whitespace
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="start">The starting position within the sequence to trim</param>
+ /// <param name="end">The end of the sequence to trim</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static void AbsoluteTrim(this VnString data, ref int start, ref int end)
+ {
+ ReadOnlySpan<char> trimmed = data.AsSpan();
+ //trim leading whitespace
+ while (start < end)
+ {
+ //If whitespace character shift start up
+ if (trimmed[start] != ' ')
+ {
+ break;
+ }
+ //Shift
+ start++;
+ }
+ //remove trailing whitespace characters
+ while (end > start)
+ {
+ //If whiterspace character shift end param down
+ if (trimmed[end - 1] != ' ')
+ {
+ break;
+ }
+ end--;
+ }
+ }
+ /// <summary>
+ /// Allows for trimming whitespace characters in a realtive sequence from
+ /// within a <see cref="VnString"/> buffer and returning the trimmed entry.
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="start">The starting position within the sequence to trim</param>
+ /// <returns>The trimmed <see cref="VnString"/> instance as a child of the original entry</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ public static VnString AbsoluteTrim(this VnString data, int start) => AbsoluteTrim(data, start, data.Length);
+ /// <summary>
+ /// Trims leading or trailing whitespace characters and returns a new child instance
+ /// without leading or trailing whitespace
+ /// </summary>
+ /// <returns>A child <see cref="VnString"/> of the current instance without leading or trailing whitespaced</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static VnString RelativeTirm(this VnString data) => AbsoluteTrim(data, 0);
+
+ /// <summary>
+ /// Allows for enumeration of segments of data within the specified <see cref="VnString"/> instance that are
+ /// split by the search parameter
+ /// </summary>
+ /// <param name="data"></param>
+ /// <param name="search">The sequence of data to delimit segments</param>
+ /// <param name="options">The options used to split the string instances</param>
+ /// <returns>An iterator to enumerate the split segments</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IEnumerable<VnString> Split(this VnString data, ReadOnlyMemory<char> search, StringSplitOptions options = StringSplitOptions.None)
+ {
+ int lowerBound = 0;
+ //Make sure the length of the search param is not 0
+ if(search.IsEmpty)
+ {
+ //Return the entire string
+ yield return data;
+ }
+ //No string options
+ else if (options == 0)
+ {
+ do
+ {
+ //Capture the first = and store argument + value
+ int splitIndex = data.IndexOf(search.Span, lowerBound);
+ //If no split index is found, then return remaining data
+ if (splitIndex == -1)
+ {
+ break;
+ }
+ yield return data[lowerBound..splitIndex];
+ //Shift the lower window to the end of the last string
+ lowerBound = splitIndex + search.Length;
+ } while (true);
+ //Return remaining data
+ yield return data[lowerBound..];
+ }
+ //Trim but do not remove empties
+ else if ((options & StringSplitOptions.RemoveEmptyEntries) == 0)
+ {
+ do
+ {
+ //Capture the first = and store argument + value
+ int splitIndex = data.IndexOf(search.Span, lowerBound);
+ //If no split index is found, then return remaining data
+ if (splitIndex == -1)
+ {
+ break;
+ }
+ //trim and return
+ yield return data.AbsoluteTrim(lowerBound, splitIndex);
+ //Shift the lower window to the end of the last string
+ lowerBound = splitIndex + search.Length;
+ } while (true);
+ //Return remaining data
+ yield return data.AbsoluteTrim(lowerBound);
+ }
+ //Remove empty entires but do not trim them
+ else if ((options & StringSplitOptions.TrimEntries) == 0)
+ {
+ do
+ {
+ //Capture the first = and store argument + value
+ int splitIndex = data.IndexOf(search.Span, lowerBound);
+ //If no split index is found, then return remaining data
+ if (splitIndex == -1)
+ {
+ break;
+ }
+ //If the split index is the next sequence, then the result is empty, so exclude it
+ else if(splitIndex > 0)
+ {
+ yield return data[lowerBound..splitIndex];
+ }
+ //Shift the lower window to the end of the last string
+ lowerBound = splitIndex + search.Length;
+ } while (true);
+ //Return remaining data if available
+ if (lowerBound < data.Length)
+ {
+ yield return data[lowerBound..];
+ }
+ }
+ //Must mean remove and trim
+ else
+ {
+ //Get stack varables to pass to trim function
+ int trimStart, trimEnd;
+ do
+ {
+ //Capture the first = and store argument + value
+ int splitIndex = data.IndexOf(search.Span, lowerBound);
+ //If no split index is found, then return remaining data
+ if (splitIndex == -1)
+ {
+ break;
+ }
+ //Get stack varables to pass to trim function
+ trimStart = lowerBound;
+ trimEnd = splitIndex; //End of the segment is the relative split index + the lower bound of the window
+ //Trim whitespace chars
+ data.AbsoluteTrim(ref trimStart, ref trimEnd);
+ //See if the string has data
+ if((trimEnd - trimStart) > 0)
+ {
+ yield return data[trimStart..trimEnd];
+ }
+ //Shift the lower window to the end of the last string
+ lowerBound = splitIndex + search.Length;
+ } while (true);
+ //Trim remaining
+ trimStart = lowerBound;
+ trimEnd = data.Length;
+ data.AbsoluteTrim(ref trimStart, ref trimEnd);
+ //If the remaining string is not empty return it
+ if ((trimEnd - trimStart) > 0)
+ {
+ yield return data[trimStart..trimEnd];
+ }
+ }
+ }
+
+ /// <summary>
+ /// Trims any leading or trailing <c>'\r'|'\n'|' '</c>(whitespace) characters from the segment
+ /// </summary>
+ /// <returns>The trimmed segment</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static VnString TrimCRLF(this VnString data)
+ {
+ ReadOnlySpan<char> trimmed = data.AsSpan();
+ int start = 0, end = trimmed.Length;
+ //trim leading \r\n chars
+ while (start < end)
+ {
+ char t = trimmed[start];
+ //If character \r or \n slice it off
+ if (t != '\r' && t != '\n' && t != ' ') {
+ break;
+ }
+ //Shift
+ start++;
+ }
+ //remove trailing crlf characters
+ while (end > start)
+ {
+ char t = trimmed[end - 1];
+ //If character \r or \n slice it off
+ if (t != '\r' && t != '\n' && t != ' ') {
+ break;
+ }
+ end--;
+ }
+ return data[start..end];
+ }
+
+ /// <summary>
+ /// Unoptimized character enumerator. You should use <see cref="VnString.AsSpan"/> to enumerate the unerlying data.
+ /// </summary>
+ /// <returns>The next character in the sequence</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IEnumerator<char> GetEnumerator(this VnString data)
+ {
+ int index = 0;
+ while (index < data.Length)
+ {
+ yield return data[index++];
+ }
+ }
+ /// <summary>
+ /// Converts the current handle to a <see cref="VnString"/>, a zero-alloc immutable wrapper
+ /// for a memory handle
+ /// </summary>
+ /// <param name="handle"></param>
+ /// <param name="length">The number of characters from the handle to reference (length of the string)</param>
+ /// <returns>The new <see cref="VnString"/> wrapper</returns>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static VnString ToVnString(this MemoryHandle<char> handle, int length)
+ {
+ if(handle.Length > int.MaxValue)
+ {
+ throw new OverflowException("The handle is larger than 2GB in size");
+ }
+ return VnString.ConsumeHandle(handle, 0, length);
+ }
+ /// <summary>
+ /// Converts the current handle to a <see cref="VnString"/>, a zero-alloc immutable wrapper
+ /// for a memory handle
+ /// </summary>
+ /// <param name="handle"></param>
+ /// <returns>The new <see cref="VnString"/> wrapper</returns>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static VnString ToVnString(this MemoryHandle<char> handle)
+ {
+ return VnString.ConsumeHandle(handle, 0, handle.IntLength);
+ }
+ /// <summary>
+ /// Converts the current handle to a <see cref="VnString"/>, a zero-alloc immutable wrapper
+ /// for a memory handle
+ /// </summary>
+ /// <param name="handle"></param>
+ /// <param name="offset">The offset in characters that represents the begining of the string</param>
+ /// <param name="length">The number of characters from the handle to reference (length of the string)</param>
+ /// <returns>The new <see cref="VnString"/> wrapper</returns>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static VnString ToVnString(this MemoryHandle<char> handle,
+#if TARGET_64_BIT
+ ulong offset,
+#else
+ int offset,
+#endif
+ int length)
+ {
+ if (handle.Length > int.MaxValue)
+ {
+ throw new OverflowException("The handle is larger than 2GB in size");
+ }
+ return VnString.ConsumeHandle(handle, offset, length);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IIndexable.cs b/lib/Utils/src/IIndexable.cs
new file mode 100644
index 0000000..129d703
--- /dev/null
+++ b/lib/Utils/src/IIndexable.cs
@@ -0,0 +1,43 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IIndexable.cs
+*
+* IIndexable.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils
+{
+ /// <summary>
+ /// Provides an interface that provides an indexer
+ /// </summary>
+ /// <typeparam name="TKey">The lookup Key</typeparam>
+ /// <typeparam name="TValue">The lookup value</typeparam>
+ public interface IIndexable<TKey, TValue>
+ {
+ /// <summary>
+ /// Gets or sets the value at the specified index in the collection
+ /// </summary>
+ /// <param name="key">The key to lookup the value at</param>
+ /// <returns>The value at the specified key</returns>
+ TValue this[TKey key] { get; set;}
+ }
+}
diff --git a/lib/Utils/src/IO/ArrayPoolStreamBuffer.cs b/lib/Utils/src/IO/ArrayPoolStreamBuffer.cs
new file mode 100644
index 0000000..df366e3
--- /dev/null
+++ b/lib/Utils/src/IO/ArrayPoolStreamBuffer.cs
@@ -0,0 +1,70 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ArrayPoolStreamBuffer.cs
+*
+* ArrayPoolStreamBuffer.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+
+namespace VNLib.Utils.IO
+{
+ internal class ArrayPoolStreamBuffer<T> : ISlindingWindowBuffer<T>
+ {
+ private readonly ArrayPool<T> _pool;
+ private T[] _buffer;
+
+ public ArrayPoolStreamBuffer(ArrayPool<T> pool, int bufferSize)
+ {
+ _pool = pool;
+ _buffer = _pool.Rent(bufferSize);
+ }
+
+ public int WindowStartPos { get; set; }
+ public int WindowEndPos { get; set; }
+
+ public Memory<T> Buffer => _buffer.AsMemory();
+
+ public void Advance(int count)
+ {
+ WindowEndPos += count;
+ }
+
+ public void AdvanceStart(int count)
+ {
+ WindowStartPos += count;
+ }
+
+ public void Close()
+ {
+ //Return buffer to pool
+ _pool.Return(_buffer);
+ _buffer = null;
+ }
+
+ public void Reset()
+ {
+ //Reset window positions
+ WindowStartPos = 0;
+ WindowEndPos = 0;
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/BackingStream.cs b/lib/Utils/src/IO/BackingStream.cs
new file mode 100644
index 0000000..cb56b09
--- /dev/null
+++ b/lib/Utils/src/IO/BackingStream.cs
@@ -0,0 +1,181 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: BackingStream.cs
+*
+* BackingStream.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Provides basic stream support sync/async stream operations to a
+ /// backing stream with virtual event methods. Provides a pass-through
+ /// as best as possbile.
+ /// </summary>
+ public abstract class BackingStream<T> : Stream where T: Stream
+ {
+ /// <summary>
+ /// The backing/underlying stream operations are being performed on
+ /// </summary>
+ protected T BaseStream { get; set; }
+ /// <summary>
+ /// A value that will cause all calls to write to throw <see cref="NotSupportedException"/>
+ /// </summary>
+ protected bool ForceReadOnly { get; set; }
+ ///<inheritdoc/>
+ public override bool CanRead => BaseStream.CanRead;
+ ///<inheritdoc/>
+ public override bool CanSeek => BaseStream.CanSeek;
+ ///<inheritdoc/>
+ public override bool CanWrite => BaseStream.CanWrite && !ForceReadOnly;
+ ///<inheritdoc/>
+ public override long Length => BaseStream.Length;
+ ///<inheritdoc/>
+ public override int WriteTimeout { get => BaseStream.WriteTimeout; set => BaseStream.WriteTimeout = value; }
+ ///<inheritdoc/>
+ public override int ReadTimeout { get => BaseStream.ReadTimeout; set => BaseStream.ReadTimeout = value; }
+ ///<inheritdoc/>
+ public override long Position { get => BaseStream.Position; set => BaseStream.Position = value; }
+ ///<inheritdoc/>
+ public override void Flush()
+ {
+ BaseStream.Flush();
+ OnFlush();
+ }
+ ///<inheritdoc/>
+ public override int Read(byte[] buffer, int offset, int count) => BaseStream.Read(buffer, offset, count);
+ ///<inheritdoc/>
+ public override int Read(Span<byte> buffer) => BaseStream.Read(buffer);
+ ///<inheritdoc/>
+ public override long Seek(long offset, SeekOrigin origin) => BaseStream.Seek(offset, origin);
+ ///<inheritdoc/>
+ public override void SetLength(long value) => BaseStream.SetLength(value);
+ ///<inheritdoc/>
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (ForceReadOnly)
+ {
+ throw new NotSupportedException("Stream is set to readonly mode");
+ }
+ BaseStream.Write(buffer, offset, count);
+ //Call onwrite function
+ OnWrite(count);
+ }
+ ///<inheritdoc/>
+ public override void Write(ReadOnlySpan<byte> buffer)
+ {
+ if (ForceReadOnly)
+ {
+ throw new NotSupportedException("Stream is set to readonly mode");
+ }
+ BaseStream.Write(buffer);
+ //Call onwrite function
+ OnWrite(buffer.Length);
+ }
+ ///<inheritdoc/>
+ public override void Close()
+ {
+ BaseStream.Close();
+ //Call on close function
+ OnClose();
+ }
+
+ /// <summary>
+ /// Raised directly after the base stream is closed, when a call to close is made
+ /// </summary>
+ protected virtual void OnClose() { }
+ /// <summary>
+ /// Raised directly after the base stream is flushed, when a call to flush is made
+ /// </summary>
+ protected virtual void OnFlush() { }
+ /// <summary>
+ /// Raised directly after a successfull write operation.
+ /// </summary>
+ /// <param name="count">The number of bytes written to the stream</param>
+ protected virtual void OnWrite(int count) { }
+
+ ///<inheritdoc/>
+ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ return BaseStream.ReadAsync(buffer, offset, count, cancellationToken);
+ }
+ ///<inheritdoc/>
+ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ return BaseStream.ReadAsync(buffer, cancellationToken);
+ }
+ ///<inheritdoc/>
+ public override void CopyTo(Stream destination, int bufferSize) => BaseStream.CopyTo(destination, bufferSize);
+ ///<inheritdoc/>
+ public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
+ {
+ return BaseStream.CopyToAsync(destination, bufferSize, cancellationToken);
+ }
+ ///<inheritdoc/>
+ public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ if (ForceReadOnly)
+ {
+ throw new NotSupportedException("Stream is set to readonly mode");
+ }
+ //We want to maintain pass through as much as possible, so supress warning
+#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
+ await BaseStream.WriteAsync(buffer, offset, count, cancellationToken);
+#pragma warning restore CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
+
+ //Call on-write and pass the number of bytes written
+ OnWrite(count);
+ }
+ ///<inheritdoc/>
+ public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ if (ForceReadOnly)
+ {
+ throw new NotSupportedException("Stream is set to readonly mode");
+ }
+ await BaseStream.WriteAsync(buffer, cancellationToken);
+ //Call on-write and pass the length
+ OnWrite(buffer.Length);
+ }
+ ///<inheritdoc/>
+ public override async Task FlushAsync(CancellationToken cancellationToken)
+ {
+ await BaseStream.FlushAsync(cancellationToken);
+ //Call onflush
+ OnFlush();
+ }
+
+ ///<inheritdoc/>
+ public override async ValueTask DisposeAsync()
+ {
+ //Dispose the base stream and await it
+ await BaseStream.DisposeAsync();
+ //Call onclose
+ OnClose();
+ //Suppress finalize
+ GC.SuppressFinalize(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/FileOperations.cs b/lib/Utils/src/IO/FileOperations.cs
new file mode 100644
index 0000000..e040da4
--- /dev/null
+++ b/lib/Utils/src/IO/FileOperations.cs
@@ -0,0 +1,105 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: FileOperations.cs
+*
+* FileOperations.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Contains cross-platform optimized filesystem operations.
+ /// </summary>
+ public static class FileOperations
+ {
+ public const int INVALID_FILE_ATTRIBUTES = -1;
+
+ [DllImport("Shlwapi", SetLastError = true, CharSet = CharSet.Auto)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ [return:MarshalAs(UnmanagedType.Bool)]
+ private static unsafe extern bool PathFileExists(char* path);
+ [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ [return:MarshalAs(UnmanagedType.I4)]
+ private static unsafe extern int GetFileAttributes(char* path);
+
+ static readonly bool IsWindows = OperatingSystem.IsWindows();
+ /// <summary>
+ /// Determines if a file exists. If application is current running in the Windows operating system, Shlwapi.PathFileExists is invoked,
+ /// otherwise <see cref="File.Exists(string?)"/> is invoked
+ /// </summary>
+ /// <param name="filePath">the path to the file</param>
+ /// <returns>True if the file can be opened, false otherwise</returns>
+ public static bool FileExists(string filePath)
+ {
+ //If windows is detected, use the unmanged function
+ if (!IsWindows)
+ {
+ return File.Exists(filePath);
+ }
+ unsafe
+ {
+ //Get a char pointer to the file path
+ fixed (char* path = filePath)
+ {
+ //Invoke the winap file function
+ return PathFileExists(path);
+ }
+ }
+ }
+
+ /// <summary>
+ /// If Windows is detected at load time, gets the attributes for the specified file.
+ /// </summary>
+ /// <param name="filePath">The path to the existing file</param>
+ /// <returns>The attributes of the file </returns>
+ /// <exception cref="PathTooLongException"></exception>
+ /// <exception cref="FileNotFoundException"></exception>
+ /// <exception cref="UnauthorizedAccessException"></exception>
+ public static FileAttributes GetAttributes(string filePath)
+ {
+ //If windows is detected, use the unmanged function
+ if (!IsWindows)
+ {
+ return File.GetAttributes(filePath);
+ }
+ unsafe
+ {
+ //Get a char pointer to the file path
+ fixed (char* path = filePath)
+ {
+ //Invoke the winap file function and cast the returned int value to file attributes
+ int attr = GetFileAttributes(path);
+ //Check for error
+ if (attr == INVALID_FILE_ATTRIBUTES)
+ {
+ throw new FileNotFoundException("The requested file was not found", filePath);
+ }
+ //Cast to file attributes and return
+ return (FileAttributes)attr;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/IDataAccumulator.cs b/lib/Utils/src/IO/IDataAccumulator.cs
new file mode 100644
index 0000000..5129a55
--- /dev/null
+++ b/lib/Utils/src/IO/IDataAccumulator.cs
@@ -0,0 +1,64 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IDataAccumulator.cs
+*
+* IDataAccumulator.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// A data structure that represents a sliding window over a buffer
+ /// for resetable forward-only reading or writing
+ /// </summary>
+ /// <typeparam name="T">The accumuation data type</typeparam>
+ public interface IDataAccumulator<T>
+ {
+ /// <summary>
+ /// Gets the number of available items within the buffer
+ /// </summary>
+ int AccumulatedSize { get; }
+ /// <summary>
+ /// The number of elements remaining in the buffer
+ /// </summary>
+ int RemainingSize { get; }
+ /// <summary>
+ /// The remaining space in the internal buffer as a contiguous segment
+ /// </summary>
+ Span<T> Remaining { get; }
+ /// <summary>
+ /// The buffer window over the accumulated data
+ /// </summary>
+ Span<T> Accumulated { get; }
+
+ /// <summary>
+ /// Advances the accumulator buffer window by the specified amount
+ /// </summary>
+ /// <param name="count">The number of elements accumulated</param>
+ void Advance(int count);
+
+ /// <summary>
+ /// Resets the internal state of the accumulator
+ /// </summary>
+ void Reset();
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/ISlindingWindowBuffer.cs b/lib/Utils/src/IO/ISlindingWindowBuffer.cs
new file mode 100644
index 0000000..ff4e142
--- /dev/null
+++ b/lib/Utils/src/IO/ISlindingWindowBuffer.cs
@@ -0,0 +1,91 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ISlindingWindowBuffer.cs
+*
+* ISlindingWindowBuffer.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Represents a sliding window buffer for reading/wiriting data
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public interface ISlindingWindowBuffer<T> : IDataAccumulator<T>
+ {
+ /// <summary>
+ /// The number of elements remaining in the buffer
+ /// </summary>
+ int IDataAccumulator<T>.RemainingSize => Buffer.Length - WindowEndPos;
+ /// <summary>
+ /// The remaining space in the internal buffer as a contiguous segment
+ /// </summary>
+ Span<T> IDataAccumulator<T>.Remaining => RemainingBuffer.Span;
+ /// <summary>
+ /// The buffer window over the accumulated data
+ /// </summary>
+ Span<T> IDataAccumulator<T>.Accumulated => AccumulatedBuffer.Span;
+ /// <summary>
+ /// Gets the number of available items within the buffer
+ /// </summary>
+ int IDataAccumulator<T>.AccumulatedSize => WindowEndPos - WindowStartPos;
+
+ /// <summary>
+ /// The starting positon of the available data within the buffer
+ /// </summary>
+ int WindowStartPos { get; }
+ /// <summary>
+ /// The ending position of the available data within the buffer
+ /// </summary>
+ int WindowEndPos { get; }
+ /// <summary>
+ /// Buffer memory wrapper
+ /// </summary>
+ Memory<T> Buffer { get; }
+
+ /// <summary>
+ /// Releases resources used by the current instance
+ /// </summary>
+ void Close();
+ /// <summary>
+ /// <para>
+ /// Advances the begining of the accumulated data window.
+ /// </para>
+ /// <para>
+ /// This method is used during reading to singal that data
+ /// has been read from the internal buffer and the
+ /// accumulator window can be shifted.
+ /// </para>
+ /// </summary>
+ /// <param name="count">The number of elements to shift by</param>
+ void AdvanceStart(int count);
+
+ /// <summary>
+ /// Gets a window within the buffer of available buffered data
+ /// </summary>
+ Memory<T> AccumulatedBuffer => Buffer[WindowStartPos..WindowEndPos];
+ /// <summary>
+ /// Gets the available buffer window to write data to
+ /// </summary>
+ Memory<T> RemainingBuffer => Buffer[WindowEndPos..];
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/IVnTextReader.cs b/lib/Utils/src/IO/IVnTextReader.cs
new file mode 100644
index 0000000..625ba78
--- /dev/null
+++ b/lib/Utils/src/IO/IVnTextReader.cs
@@ -0,0 +1,72 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IVnTextReader.cs
+*
+* IVnTextReader.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Text;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Represents a streaming text reader with internal buffers
+ /// </summary>
+ public interface IVnTextReader
+ {
+ /// <summary>
+ /// The base stream to read data from
+ /// </summary>
+ Stream BaseStream { get; }
+ /// <summary>
+ /// The character encoding used by the TextReader
+ /// </summary>
+ Encoding Encoding { get; }
+ /// <summary>
+ /// Number of available bytes of buffered data within the current buffer window
+ /// </summary>
+ int Available { get; }
+ /// <summary>
+ /// Gets or sets the line termination used to deliminate a line of data
+ /// </summary>
+ ReadOnlyMemory<byte> LineTermination { get; }
+ /// <summary>
+ /// The unread/available data within the internal buffer
+ /// </summary>
+ Span<byte> BufferedDataWindow { get; }
+ /// <summary>
+ /// Shifts the sliding buffer window by the specified number of bytes.
+ /// </summary>
+ /// <param name="count">The number of bytes read from the buffer</param>
+ void Advance(int count);
+ /// <summary>
+ /// Reads data from the stream into the remaining buffer space for processing
+ /// </summary>
+ void FillBuffer();
+ /// <summary>
+ /// Compacts the available buffer space back to the begining of the buffer region
+ /// and determines if there is room for more data to be buffered
+ /// </summary>
+ /// <returns>The remaining buffer space if any</returns>
+ ERRNO CompactBufferWindow();
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/InMemoryTemplate.cs b/lib/Utils/src/IO/InMemoryTemplate.cs
new file mode 100644
index 0000000..ae8bf79
--- /dev/null
+++ b/lib/Utils/src/IO/InMemoryTemplate.cs
@@ -0,0 +1,196 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: InMemoryTemplate.cs
+*
+* InMemoryTemplate.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Represents a lazily loaded file stored in memory, with a change mointor
+ /// that reloads the template if the file was modified in the filesystem
+ /// </summary>
+ public abstract class InMemoryTemplate : VnDisposeable
+ {
+ protected ManualResetEventSlim TemplateLock;
+ private readonly FileSystemWatcher? Watcher;
+ private bool Modified;
+ private VnMemoryStream templateBuffer;
+ protected readonly FileInfo TemplateFile;
+
+ /// <summary>
+ /// Gets the name of the template
+ /// </summary>
+ public abstract string TemplateName { get; }
+
+ /// <summary>
+ /// Creates a new in-memory copy of a file that will detect changes and refresh
+ /// </summary>
+ /// <param name="listenForChanges">Should changes to the template file be moniored for changes, and reloaded as necessary</param>
+ /// <param name="path">The path of the file template</param>
+ protected InMemoryTemplate(string path, bool listenForChanges = true)
+ {
+ TemplateFile = new FileInfo(path);
+ TemplateLock = new(true);
+ //Make sure the file exists
+ if (!TemplateFile.Exists)
+ {
+ throw new FileNotFoundException("Template file does not exist");
+ }
+ if (listenForChanges)
+ {
+ //Setup a watcher to reload the template when modified
+ Watcher = new FileSystemWatcher(TemplateFile.DirectoryName!)
+ {
+ EnableRaisingEvents = true,
+ IncludeSubdirectories = false,
+ NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size
+ };
+ Watcher.Changed += Watcher_Changed;
+ }
+ //Set modified flag to make sure the template is read on first use
+ this.Modified = true;
+ }
+
+ private void Watcher_Changed(object sender, FileSystemEventArgs e)
+ {
+ //Make sure the event was raied for this template
+ if (!e.FullPath.Equals(TemplateFile.FullName, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+ TemplateLock.Reset();
+ try
+ {
+ //Set modified flag
+ Modified = true;
+ //Refresh the fileinfo object
+ TemplateFile.Refresh();
+ //Invoke onmodifed function
+ OnModifed();
+ }
+ finally
+ {
+ TemplateLock.Set();
+ }
+ }
+
+ /// <summary>
+ /// Gets a cached copy of the template data
+ /// </summary>
+ protected VnMemoryStream GetTemplateData()
+ {
+ //Make sure access is synchronized incase the file gets updated during access on another thread
+ TemplateLock.Wait();
+ //Determine if the file has been modified and needs to be reloaded
+ if (Modified)
+ {
+ TemplateLock.Reset();
+ try
+ {
+ //Read a new copy of the templte into mem
+ ReadFile();
+ }
+ finally
+ {
+ TemplateLock.Set();
+ }
+ }
+ //Return a copy of the memory stream
+ return templateBuffer.GetReadonlyShallowCopy();
+ }
+ /// <summary>
+ /// Updates the internal copy of the file to its memory representation
+ /// </summary>
+ protected void ReadFile()
+ {
+ //Open the file stream
+ using FileStream fs = TemplateFile.OpenRead();
+ //Dispose the old template buffer
+ templateBuffer?.Dispose();
+ //Create a new stream for storing the cached copy
+ VnMemoryStream newBuf = new();
+ try
+ {
+ fs.CopyTo(newBuf, null);
+ }
+ catch
+ {
+ newBuf.Dispose();
+ throw;
+ }
+ //Create the readonly copy
+ templateBuffer = VnMemoryStream.CreateReadonly(newBuf);
+ //Clear the modified flag
+ Modified = false;
+ }
+ /// <summary>
+ /// Updates the internal copy of the file to its memory representation, asynchronously
+ /// </summary>
+ /// <param name="cancellationToken"></param>
+ /// <returns>A task that completes when the file has been copied into memory</returns>
+ protected async Task ReadFileAsync(CancellationToken cancellationToken = default)
+ {
+ //Open the file stream
+ await using FileStream fs = TemplateFile.OpenRead();
+ //Dispose the old template buffer
+ templateBuffer?.Dispose();
+ //Create a new stream for storing the cached copy
+ VnMemoryStream newBuf = new();
+ try
+ {
+ //Copy async
+ await fs.CopyToAsync(newBuf, 8192, Memory.Memory.Shared, cancellationToken);
+ }
+ catch
+ {
+ newBuf.Dispose();
+ throw;
+ }
+ //Create the readonly copy
+ templateBuffer = VnMemoryStream.CreateReadonly(newBuf);
+ //Clear the modified flag
+ Modified = false;
+ }
+
+ /// <summary>
+ /// Invoked when the template file has been modifed. Note: This event is raised
+ /// while the <see cref="TemplateLock"/> is held.
+ /// </summary>
+ protected abstract void OnModifed();
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ //Dispose the watcher
+ Watcher?.Dispose();
+ //free the stream
+ templateBuffer?.Dispose();
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/IsolatedStorageDirectory.cs b/lib/Utils/src/IO/IsolatedStorageDirectory.cs
new file mode 100644
index 0000000..65460ff
--- /dev/null
+++ b/lib/Utils/src/IO/IsolatedStorageDirectory.cs
@@ -0,0 +1,154 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IsolatedStorageDirectory.cs
+*
+* IsolatedStorageDirectory.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.IO.IsolatedStorage;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Represents an open directory within an <see cref="IsolatedStorageFile"/> store for which files can be created, opened, or deleted.
+ /// </summary>
+ public sealed class IsolatedStorageDirectory : IsolatedStorage
+ {
+ private readonly string DirectoryPath;
+ private readonly IsolatedStorageFile Storage;
+ /// <summary>
+ /// Creates a new <see cref="IsolatedStorageDirectory"/> within the specified file using the directory name.
+ /// </summary>
+ /// <param name="storage">A configured and open <see cref="IsolatedStorageFile"/></param>
+ /// <param name="dir">The directory name to open or create within the store</param>
+ public IsolatedStorageDirectory(IsolatedStorageFile storage, string dir)
+ {
+ this.Storage = storage;
+ this.DirectoryPath = dir;
+ //If the directory doesnt exist, create it
+ if (!this.Storage.DirectoryExists(dir))
+ this.Storage.CreateDirectory(dir);
+ }
+
+ private IsolatedStorageDirectory(IsolatedStorageDirectory parent, string dirName)
+ {
+ //Store ref to parent dir
+ Parent = parent;
+ //Referrence store
+ this.Storage = parent.Storage;
+ //Add the name of this dir to the end of the specified dir path
+ this.DirectoryPath = Path.Combine(parent.DirectoryPath, dirName);
+ }
+
+ /// <summary>
+ /// Creates a file by its path name within the currnet directory
+ /// </summary>
+ /// <param name="fileName">The name of the file</param>
+ /// <returns>The open file</returns>
+ /// <exception cref="IsolatedStorageException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="DirectoryNotFoundException"></exception>
+ public IsolatedStorageFileStream CreateFile(string fileName)
+ {
+ return this.Storage.CreateFile(Path.Combine(DirectoryPath, fileName));
+ }
+ /// <summary>
+ /// Removes a file from the current directory
+ /// </summary>
+ /// <param name="fileName">The path of the file to remove</param>
+ /// <exception cref="IsolatedStorageException"></exception>
+ public void DeleteFile(string fileName)
+ {
+ this.Storage.DeleteFile(Path.Combine(this.DirectoryPath, fileName));
+ }
+ /// <summary>
+ /// Opens a file that exists within the current directory
+ /// </summary>
+ /// <param name="fileName">Name with extension of the file</param>
+ /// <param name="mode">File mode</param>
+ /// <param name="access">File access</param>
+ /// <returns>The open <see cref="IsolatedStorageFileStream"/> from the current directory</returns>
+ public IsolatedStorageFileStream OpenFile(string fileName, FileMode mode, FileAccess access)
+ {
+ return this.Storage.OpenFile(Path.Combine(DirectoryPath, fileName), mode, access);
+ }
+ /// <summary>
+ /// Opens a file that exists within the current directory
+ /// </summary>
+ /// <param name="fileName">Name with extension of the file</param>
+ /// <param name="mode">File mode</param>
+ /// <param name="access">File access</param>
+ /// <param name="share">The file shareing mode</param>
+ /// <returns>The open <see cref="IsolatedStorageFileStream"/> from the current directory</returns>
+ public IsolatedStorageFileStream OpenFile(string fileName, FileMode mode, FileAccess access, FileShare share)
+ {
+ return this.Storage.OpenFile(Path.Combine(DirectoryPath, fileName), mode, access, share);
+ }
+
+ /// <summary>
+ /// Determiens if the specified file path refers to an existing file within the directory
+ /// </summary>
+ /// <param name="fileName">The name of the file to search for</param>
+ /// <returns>True if the file exists within the current directory</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="IsolatedStorageException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ public bool FileExists(string fileName)
+ {
+ return this.Storage.FileExists(Path.Combine(this.DirectoryPath, fileName));
+ }
+
+ /// <summary>
+ /// Removes the directory and its contents from the store
+ /// </summary>
+ public override void Remove()
+ {
+ Storage.DeleteDirectory(this.DirectoryPath);
+ }
+
+ public override long AvailableFreeSpace => Storage.AvailableFreeSpace;
+ public override long Quota => Storage.Quota;
+ public override long UsedSize => Storage.UsedSize;
+ public override bool IncreaseQuotaTo(long newQuotaSize) => Storage.IncreaseQuotaTo(newQuotaSize);
+
+ /// <summary>
+ /// The parent <see cref="IsolatedStorageDirectory"/> this directory is a child within. null if there are no parent directories
+ /// above this dir
+ /// </summary>
+
+ public IsolatedStorageDirectory? Parent { get; }
+#nullable disable
+
+ /// <summary>
+ /// Creates a child directory within the current directory
+ /// </summary>
+ /// <param name="directoryName">The name of the child directory</param>
+ /// <returns>A new <see cref="IsolatedStorageDirectory"/> for which <see cref="IsolatedStorageFileStream"/>s can be opened/created</returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ public IsolatedStorageDirectory CreateChildDirectory(string directoryName)
+ {
+ return new IsolatedStorageDirectory(this, directoryName);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/SlidingWindowBufferExtensions.cs b/lib/Utils/src/IO/SlidingWindowBufferExtensions.cs
new file mode 100644
index 0000000..0509061
--- /dev/null
+++ b/lib/Utils/src/IO/SlidingWindowBufferExtensions.cs
@@ -0,0 +1,213 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: SlidingWindowBufferExtensions.cs
+*
+* SlidingWindowBufferExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+using System.Runtime.CompilerServices;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Extention methods for <see cref="ISlindingWindowBuffer{T}"/>
+ /// </summary>
+ public static class SlidingWindowBufferExtensions
+ {
+ /// <summary>
+ /// Shifts/resets the current buffered data window down to the
+ /// begining of the buffer if the buffer window is shifted away
+ /// from the begining.
+ /// </summary>
+ /// <returns>The number of bytes of available space in the buffer</returns>
+ public static ERRNO CompactBufferWindow<T>(this ISlindingWindowBuffer<T> sBuf)
+ {
+ //Nothing to compact if the starting data pointer is at the beining of the window
+ if (sBuf.WindowStartPos > 0)
+ {
+ //Get span over engire buffer
+ Span<T> buffer = sBuf.Buffer.Span;
+ //Get data within window
+ Span<T> usedData = sBuf.Accumulated;
+ //Copy remaining to the begining of the buffer
+ usedData.CopyTo(buffer);
+
+ //Reset positions, then advance to the specified size
+ sBuf.Reset();
+ sBuf.Advance(usedData.Length);
+ }
+ //Return the number of bytes of available space
+ return sBuf.RemainingSize;
+ }
+
+ /// <summary>
+ /// Appends the specified data to the end of the buffer
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="sBuf"></param>
+ /// <param name="val">The value to append to the end of the buffer</param>
+ /// <exception cref="IndexOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Append<T>(this IDataAccumulator<T> sBuf, T val)
+ {
+ //Set the value at first position
+ sBuf.Remaining[0] = val;
+ //Advance by 1
+ sBuf.Advance(1);
+ }
+ /// <summary>
+ /// Appends the specified data to the end of the buffer
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="sBuf"></param>
+ /// <param name="val">The value to append to the end of the buffer</param>
+ /// <exception cref="ArgumentException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Append<T>(this IDataAccumulator<T> sBuf, ReadOnlySpan<T> val)
+ {
+ val.CopyTo(sBuf.Remaining);
+ sBuf.Advance(val.Length);
+ }
+ /// <summary>
+ /// Formats and appends a value type to the accumulator with proper endianess
+ /// </summary>
+ /// <typeparam name="T">The value type to appent</typeparam>
+ /// <param name="accumulator">The binary accumulator to append</param>
+ /// <param name="value">The value type to append</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Append<T>(this IDataAccumulator<byte> accumulator, T value) where T: unmanaged
+ {
+ //Use forward reader for the memory extension to append a value type to a binary accumulator
+ ForwardOnlyWriter<byte> w = new(accumulator.Remaining);
+ w.Append(value);
+ accumulator.Advance(w.Written);
+ }
+
+ /// <summary>
+ /// Attempts to write as much data as possible to the remaining space
+ /// in the buffer and returns the number of bytes accumulated.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="accumulator"></param>
+ /// <param name="value">The value to accumulate</param>
+ /// <returns>The number of bytes accumulated</returns>
+ public static ERRNO TryAccumulate<T>(this IDataAccumulator<T> accumulator, ReadOnlySpan<T> value)
+ {
+ //Calc data size and reserve space for final crlf
+ int dataToCopy = Math.Min(value.Length, accumulator.RemainingSize);
+
+ //Write as much data as possible
+ accumulator.Append(value[..dataToCopy]);
+
+ //Return number of bytes not written
+ return dataToCopy;
+ }
+
+ /// <summary>
+ /// Appends a <see cref="ISpanFormattable"/> instance to the end of the accumulator
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="accumulator"></param>
+ /// <param name="formattable">The formattable instance to write to the accumulator</param>
+ /// <param name="format">The format arguments</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Append<T>(this IDataAccumulator<char> accumulator, in T formattable, ReadOnlySpan<char> format = default) where T : struct, ISpanFormattable
+ {
+ ForwardOnlyWriter<char> writer = new(accumulator.Remaining);
+ writer.Append(formattable, format);
+ accumulator.Advance(writer.Written);
+ }
+
+ /// <summary>
+ /// Uses the remaining data buffer to compile a <see cref="IStringSerializeable"/>
+ /// instance, then advances the accumulator by the number of characters used.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="accumulator"></param>
+ /// <param name="compileable">The <see cref="IStringSerializeable"/> instance to compile</param>
+ public static void Append<T>(this IDataAccumulator<char> accumulator, in T compileable) where T : IStringSerializeable
+ {
+ //Write directly to the remaining space
+ int written = compileable.Compile(accumulator.Remaining);
+ //Advance the writer
+ accumulator.Advance(written);
+ }
+
+ /// <summary>
+ /// Reads available data from the current window and writes as much as possible it to the supplied buffer
+ /// and advances the buffer window
+ /// </summary>
+ /// <typeparam name="T">Element type</typeparam>
+ /// <param name="sBuf"></param>
+ /// <param name="buffer">The output buffer to write data to</param>
+ /// <returns>The number of elements written to the buffer</returns>
+ public static ERRNO Read<T>(this ISlindingWindowBuffer<T> sBuf, in Span<T> buffer)
+ {
+ //Calculate the amount of data to copy
+ int dataToCopy = Math.Min(buffer.Length, sBuf.AccumulatedSize);
+ //Copy the data to the buffer
+ sBuf.Accumulated[..dataToCopy].CopyTo(buffer);
+ //Advance the window
+ sBuf.AdvanceStart(dataToCopy);
+ //Return the number of bytes copied
+ return dataToCopy;
+ }
+
+ /// <summary>
+ /// Fills the remaining window space of the current accumulator with
+ /// data from the specified stream asynchronously.
+ /// </summary>
+ /// <param name="accumulator"></param>
+ /// <param name="input">The stream to read data from</param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>A value task representing the operation</returns>
+ public static async ValueTask AccumulateDataAsync(this ISlindingWindowBuffer<byte> accumulator, Stream input, CancellationToken cancellationToken)
+ {
+ //Get a buffer from the end of the current window to the end of the buffer
+ Memory<byte> bufWindow = accumulator.RemainingBuffer;
+ //Read from stream async
+ int read = await input.ReadAsync(bufWindow, cancellationToken);
+ //Update the end of the buffer window to the end of the read data
+ accumulator.Advance(read);
+ }
+ /// <summary>
+ /// Fills the remaining window space of the current accumulator with
+ /// data from the specified stream.
+ /// </summary>
+ /// <param name="accumulator"></param>
+ /// <param name="input">The stream to read data from</param>
+ public static void AccumulateData(this IDataAccumulator<byte> accumulator, Stream input)
+ {
+ //Get a buffer from the end of the current window to the end of the buffer
+ Span<byte> bufWindow = accumulator.Remaining;
+ //Read from stream async
+ int read = input.Read(bufWindow);
+ //Update the end of the buffer window to the end of the read data
+ accumulator.Advance(read);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/TemporayIsolatedFile.cs b/lib/Utils/src/IO/TemporayIsolatedFile.cs
new file mode 100644
index 0000000..3bee92b
--- /dev/null
+++ b/lib/Utils/src/IO/TemporayIsolatedFile.cs
@@ -0,0 +1,57 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: TemporayIsolatedFile.cs
+*
+* TemporayIsolatedFile.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.IO.IsolatedStorage;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Allows for temporary files to be generated, used, then removed from an <see cref="IsolatedStorageFile"/>
+ /// </summary>
+ public sealed class TemporayIsolatedFile : BackingStream<IsolatedStorageFileStream>
+ {
+ private readonly IsolatedStorageDirectory Storage;
+ private readonly string Filename;
+ /// <summary>
+ /// Creates a new temporary filestream within the specified <see cref="IsolatedStorageFile"/>
+ /// </summary>
+ /// <param name="storage">The file store to genreate temporary files within</param>
+ public TemporayIsolatedFile(IsolatedStorageDirectory storage)
+ {
+ //Store ref
+ this.Storage = storage;
+ //Creaet a new random filename
+ this.Filename = Path.GetRandomFileName();
+ //try to created a new file within the isolaged storage
+ this.BaseStream = storage.CreateFile(this.Filename);
+ }
+ protected override void OnClose()
+ {
+ //Remove the file from the storage
+ Storage.DeleteFile(this.Filename);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/VnMemoryStream.cs b/lib/Utils/src/IO/VnMemoryStream.cs
new file mode 100644
index 0000000..4e8a2b3
--- /dev/null
+++ b/lib/Utils/src/IO/VnMemoryStream.cs
@@ -0,0 +1,469 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: VnMemoryStream.cs
+*
+* VnMemoryStream.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Runtime.InteropServices;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.IO
+{
+
+ using Utils.Memory;
+
+ /// <summary>
+ /// Provides an unmanaged memory stream. Desigend to help reduce garbage collector load for
+ /// high frequency memory operations. Similar to <see cref="UnmanagedMemoryStream"/>
+ /// </summary>
+ public sealed class VnMemoryStream : Stream, ICloneable
+ {
+ private long _position;
+ private long _length;
+ //Memory
+ private readonly MemoryHandle<byte> _buffer;
+ private bool IsReadonly;
+ //Default owns handle
+ private readonly bool OwnsHandle = true;
+
+ /// <summary>
+ /// Creates a new <see cref="VnMemoryStream"/> pointing to the begining of memory, and consumes the handle.
+ /// </summary>
+ /// <param name="handle"><see cref="MemoryHandle{T}"/> to consume</param>
+ /// <param name="length">Length of the stream</param>
+ /// <param name="readOnly">Should the stream be readonly?</param>
+ /// <exception cref="ArgumentException"></exception>
+ /// <returns>A <see cref="VnMemoryStream"/> wrapper to access the handle data</returns>
+ public static VnMemoryStream ConsumeHandle(MemoryHandle<byte> handle, Int64 length, bool readOnly)
+ {
+ handle.ThrowIfClosed();
+ return new VnMemoryStream(handle, length, readOnly, true);
+ }
+
+ /// <summary>
+ /// Converts a writable <see cref="VnMemoryStream"/> to readonly to allow shallow copies
+ /// </summary>
+ /// <param name="stream">The stream to make readonly</param>
+ /// <returns>The readonly stream</returns>
+ public static VnMemoryStream CreateReadonly(VnMemoryStream stream)
+ {
+ //Set the readonly flag
+ stream.IsReadonly = true;
+ //Return the stream
+ return stream;
+ }
+
+ /// <summary>
+ /// Creates a new memory stream
+ /// </summary>
+ public VnMemoryStream() : this(Memory.Shared) { }
+ /// <summary>
+ /// Create a new memory stream where buffers will be allocated from the specified heap
+ /// </summary>
+ /// <param name="heap"><see cref="PrivateHeap"/> to allocate memory from</param>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ public VnMemoryStream(IUnmangedHeap heap) : this(heap, 0, false) { }
+
+ /// <summary>
+ /// Creates a new memory stream and pre-allocates the internal
+ /// buffer of the specified size on the specified heap to avoid resizing.
+ /// </summary>
+ /// <param name="heap"><see cref="PrivateHeap"/> to allocate memory from</param>
+ /// <param name="bufferSize">Number of bytes (length) of the stream if known</param>
+ /// <param name="zero">Zero memory allocations during buffer expansions</param>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public VnMemoryStream(IUnmangedHeap heap, long bufferSize, bool zero)
+ {
+ _ = heap ?? throw new ArgumentNullException(nameof(heap));
+ _buffer = heap.Alloc<byte>(bufferSize, zero);
+ }
+
+ /// <summary>
+ /// Creates a new memory stream from the data provided
+ /// </summary>
+ /// <param name="heap"><see cref="PrivateHeap"/> to allocate memory from</param>
+ /// <param name="data">Initial data</param>
+ public VnMemoryStream(IUnmangedHeap heap, ReadOnlySpan<byte> data)
+ {
+ _ = heap ?? throw new ArgumentNullException(nameof(heap));
+ //Alloc the internal buffer to match the data stream
+ _buffer = heap.AllocAndCopy(data);
+ //Set length
+ _length = data.Length;
+ //Position will default to 0 cuz its dotnet :P
+ return;
+ }
+
+ /// <summary>
+ /// WARNING: Dangerous constructor, make sure read-only and owns hanlde are set accordingly
+ /// </summary>
+ /// <param name="buffer">The buffer to referrence directly</param>
+ /// <param name="length">The length property of the stream</param>
+ /// <param name="readOnly">Is the stream readonly (should mostly be true!)</param>
+ /// <param name="ownsHandle">Does the new stream own the memory -> <paramref name="buffer"/></param>
+ private VnMemoryStream(MemoryHandle<byte> buffer, long length, bool readOnly, bool ownsHandle)
+ {
+ OwnsHandle = ownsHandle;
+ _buffer = buffer; //Consume the handle
+ _length = length; //Store length of the buffer
+ IsReadonly = readOnly;
+ }
+
+ /// <summary>
+ /// UNSAFE Number of bytes between position and length. Never negative
+ /// </summary>
+ private long LenToPosDiff => Math.Max(_length - _position, 0);
+
+ /// <summary>
+ /// If the current stream is a readonly stream, creates an unsafe shallow copy for reading only.
+ /// </summary>
+ /// <returns>New stream shallow copy of the internal stream</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ public VnMemoryStream GetReadonlyShallowCopy()
+ {
+ //Create a new readonly copy (stream does not own the handle)
+ return !IsReadonly
+ ? throw new NotSupportedException("This stream is not readonly. Cannot create shallow copy on a mutable stream")
+ : new VnMemoryStream(_buffer, _length, true, false);
+ }
+
+ /// <summary>
+ /// Writes data directly to the destination stream from the internal buffer
+ /// without allocating or copying any data.
+ /// </summary>
+ /// <param name="destination">The stream to write data to</param>
+ /// <param name="bufferSize">The size of the chunks to write to the destination stream</param>
+ /// <exception cref="IOException"></exception>
+ public override void CopyTo(Stream destination, int bufferSize)
+ {
+ _ = destination ?? throw new ArgumentNullException(nameof(destination));
+
+ if (!destination.CanWrite)
+ {
+ throw new IOException("The destinaion stream is not writeable");
+ }
+
+ do
+ {
+ //Calc the remaining bytes to read no larger than the buffer size
+ int bytesToRead = (int)Math.Min(LenToPosDiff, bufferSize);
+
+ //Create a span wrapper by using the offet function to support memory handles larger than 2gb
+ ReadOnlySpan<byte> span = _buffer.GetOffsetSpan(_position, bytesToRead);
+
+ destination.Write(span);
+
+ //Update position
+ _position += bytesToRead;
+
+ } while (LenToPosDiff > 0);
+ }
+
+ /// <summary>
+ /// Allocates a temporary buffer of the desired size, copies data from the internal
+ /// buffer and writes it to the destination buffer asynchronously.
+ /// </summary>
+ /// <param name="destination">The stream to write output data to</param>
+ /// <param name="bufferSize">The size of the buffer to use when copying data</param>
+ /// <param name="cancellationToken">A token to cancel the opreation</param>
+ /// <returns>A task that resolves when the remaining data in the stream has been written to the destination</returns>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
+ {
+ _ = destination ?? throw new ArgumentNullException(nameof(destination));
+
+ if (!destination.CanWrite)
+ {
+ throw new IOException("The destinaion stream is not writeable");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ /*
+ * Alloc temp copy buffer. This is a requirement because
+ * the stream may be larger than an int32 so it must be
+ * copied by segment
+ */
+
+ using VnTempBuffer<byte> copyBuffer = new(bufferSize);
+
+ do
+ {
+ //read from internal stream
+ int read = Read(copyBuffer);
+
+ if(read <= 0)
+ {
+ break;
+ }
+
+ //write async
+ await destination.WriteAsync(copyBuffer.AsMemory(0, read), cancellationToken);
+
+ } while (true);
+
+ }
+
+ /// <summary>
+ /// <inheritdoc/>
+ /// <para>
+ /// This property is always true
+ /// </para>
+ /// </summary>
+ public override bool CanRead => true;
+ /// <summary>
+ /// <inheritdoc/>
+ /// <para>
+ /// This propery is always true
+ /// </para>
+ /// </summary>
+ public override bool CanSeek => true;
+ /// <summary>
+ /// True unless the stream is (or has been converted to) a readonly
+ /// stream.
+ /// </summary>
+ public override bool CanWrite => !IsReadonly;
+ ///<inheritdoc/>
+ public override long Length => _length;
+ ///<inheritdoc/>
+ public override bool CanTimeout => false;
+
+ ///<inheritdoc/>
+ public override long Position
+ {
+ get => _position;
+ set => Seek(value, SeekOrigin.Begin);
+ }
+ /// <summary>
+ /// Closes the stream and frees the internal allocated memory blocks
+ /// </summary>
+ public override void Close()
+ {
+ //Only dispose buffer if we own it
+ if (OwnsHandle)
+ {
+ _buffer.Dispose();
+ }
+ }
+ ///<inheritdoc/>
+ public override void Flush() { }
+ // Override to reduce base class overhead
+ ///<inheritdoc/>
+ public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+ ///<inheritdoc/>
+ public override int Read(byte[] buffer, int offset, int count) => Read(new Span<byte>(buffer, offset, count));
+ ///<inheritdoc/>
+ public override int Read(Span<byte> buffer)
+ {
+ if (buffer.Length == 0)
+ {
+ return 0;
+ }
+ //Number of bytes to read from memory buffer
+ int bytesToRead = checked((int)Math.Min(LenToPosDiff, buffer.Length));
+ //Copy bytes to buffer
+ Memory.Copy(_buffer, _position, buffer, 0, bytesToRead);
+ //Increment buffer position
+ _position += bytesToRead;
+ //Bytestoread should never be larger than int.max because span length is an integer
+ return bytesToRead;
+ }
+
+ /*
+ * Async reading will always run synchronously in a memory stream,
+ * so overrides are just so avoid base class overhead
+ */
+ ///<inheritdoc/>
+ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ //Read synchronously and return a completed task
+ int read = Read(buffer.Span);
+ return ValueTask.FromResult(read);
+ }
+ ///<inheritdoc/>
+ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ //Read synchronously and return a completed task
+ int read = Read(buffer.AsSpan(offset, count));
+ return Task.FromResult(read);
+ }
+ ///<inheritdoc/>
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ if (offset < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), "Offset cannot be less than 0");
+ }
+ switch (origin)
+ {
+ case SeekOrigin.Begin:
+ //Length will never be greater than int.Max so output will never exceed int.max
+ _position = Math.Min(_length, offset);
+ return _position;
+ case SeekOrigin.Current:
+ long newPos = _position + offset;
+ //Length will never be greater than int.Max so output will never exceed length
+ _position = Math.Min(_length, newPos);
+ return newPos;
+ case SeekOrigin.End:
+ long real_index = _length - offset;
+ //If offset moves the position negative, just set the position to 0 and continue
+ _position = Math.Min(real_index, 0);
+ return real_index;
+ default:
+ throw new ArgumentException("Stream operation is not supported on current stream");
+ }
+ }
+
+
+ /// <summary>
+ /// Resizes the internal buffer to the exact size (in bytes) of the
+ /// value argument. A value of 0 will free the entire buffer. A value
+ /// greater than zero will resize the buffer (and/or alloc)
+ /// </summary>
+ /// <param name="value">The size of the stream (and internal buffer)</param>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public override void SetLength(long value)
+ {
+ if (IsReadonly)
+ {
+ throw new NotSupportedException("This stream is readonly");
+ }
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "Value cannot be less than 0");
+ }
+ //Resize the buffer to the specified length
+ _buffer.Resize(value);
+ //Set length
+ _length = value;
+ //Make sure the position is not pointing outside of the buffer
+ _position = Math.Min(_position, _length);
+ return;
+ }
+ ///<inheritdoc/>
+ public override void Write(byte[] buffer, int offset, int count) => Write(new ReadOnlySpan<byte>(buffer, offset, count));
+ ///<inheritdoc/>
+ public override void Write(ReadOnlySpan<byte> buffer)
+ {
+ if (IsReadonly)
+ {
+ throw new NotSupportedException("Write operation is not allowed on readonly stream!");
+ }
+ //Calculate the new final position
+ long newPos = (_position + buffer.Length);
+ //Determine if the buffer needs to be expanded
+ if (buffer.Length > LenToPosDiff)
+ {
+ //Expand buffer if required
+ _buffer.ResizeIfSmaller(newPos);
+ //Update length
+ _length = newPos;
+ }
+ //Copy the input buffer to the internal buffer
+ Memory.Copy(buffer, _buffer, _position);
+ //Update the position
+ _position = newPos;
+ return;
+ }
+ ///<inheritdoc/>
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ //Write synchronously and return a completed task
+ Write(buffer, offset, count);
+ return Task.CompletedTask;
+ }
+ ///<inheritdoc/>
+ public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ //Write synchronously and return a completed task
+ Write(buffer.Span);
+ return ValueTask.CompletedTask;
+ }
+ ///<inheritdoc/>
+ public override void WriteByte(byte value)
+ {
+ Span<byte> buf = MemoryMarshal.CreateSpan(ref value, 1);
+ Write(buf);
+ }
+
+ /// <summary>
+ /// Allocates and copies internal buffer to new managed byte[]
+ /// </summary>
+ /// <returns>Copy of internal buffer</returns>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ public byte[] ToArray()
+ {
+ //Alloc a new array of the size of the internal buffer
+ byte[] data = new byte[_length];
+ //Copy data from the internal buffer to the output buffer
+ _buffer.Span.CopyTo(data);
+ return data;
+
+ }
+ /// <summary>
+ /// Returns a <see cref="ReadOnlySpan{T}"/> window over the data within the entire stream
+ /// </summary>
+ /// <returns>A <see cref="ReadOnlySpan{T}"/> of the data within the entire stream</returns>
+ /// <exception cref="OverflowException"></exception>
+ public ReadOnlySpan<byte> AsSpan()
+ {
+ ReadOnlySpan<byte> output = _buffer.Span;
+ return output[..(int)_length];
+ }
+
+ /// <summary>
+ /// If the current stream is a readonly stream, creates a shallow copy for reading only.
+ /// </summary>
+ /// <returns>New stream shallow copy of the internal stream</returns>
+ /// <exception cref="InvalidOperationException"></exception>
+ public object Clone() => GetReadonlyShallowCopy();
+
+ /*
+ * Override the Dispose async method to avoid the base class overhead
+ * and task allocation since this will always be a syncrhonous
+ * operation (freeing memory)
+ */
+
+ ///<inheritdoc/>
+ public override ValueTask DisposeAsync()
+ {
+ //Dispose and return completed task
+ base.Dispose(true);
+ GC.SuppressFinalize(this);
+ return ValueTask.CompletedTask;
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/VnStreamReader.cs b/lib/Utils/src/IO/VnStreamReader.cs
new file mode 100644
index 0000000..70b9734
--- /dev/null
+++ b/lib/Utils/src/IO/VnStreamReader.cs
@@ -0,0 +1,180 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: VnStreamReader.cs
+*
+* VnStreamReader.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Text;
+using System.Buffers;
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Binary based buffered text reader, optimized for reading network streams
+ /// </summary>
+ public class VnStreamReader : TextReader, IVnTextReader
+ {
+ private bool disposedValue;
+
+ private readonly ISlindingWindowBuffer<byte> _buffer;
+ ///<inheritdoc/>
+ public virtual Stream BaseStream { get; }
+ ///<inheritdoc/>
+ public Encoding Encoding { get; }
+
+ /// <summary>
+ /// Number of available bytes of buffered data within the current buffer window
+ /// </summary>
+ public int Available => _buffer.AccumulatedSize;
+ /// <summary>
+ /// Gets or sets the line termination used to deliminate a line of data
+ /// </summary>
+ public ReadOnlyMemory<byte> LineTermination { get; set; }
+ Span<byte> IVnTextReader.BufferedDataWindow => _buffer.Accumulated;
+
+ /// <summary>
+ /// Creates a new <see cref="TextReader"/> that reads encoded data from the base.
+ /// Internal buffers will be alloced from <see cref="ArrayPool{T}.Shared"/>
+ /// </summary>
+ /// <param name="baseStream">The underlying stream to read data from</param>
+ /// <param name="enc">The <see cref="Encoding"/> to use when reading from the stream</param>
+ /// <param name="bufferSize">The size of the internal binary buffer</param>
+ public VnStreamReader(Stream baseStream, Encoding enc, int bufferSize)
+ {
+ BaseStream = baseStream;
+ Encoding = enc;
+ //Init a new buffer
+ _buffer = InitializeBuffer(bufferSize);
+ }
+
+ /// <summary>
+ /// Invoked by the constuctor method to allocte the internal buffer with the specified buffer size.
+ /// </summary>
+ /// <param name="bufferSize">The requested size of the buffer to alloc</param>
+ /// <remarks>By default requests the buffer from the <see cref="ArrayPool{T}.Shared"/> instance</remarks>
+ protected virtual ISlindingWindowBuffer<byte> InitializeBuffer(int bufferSize) => new ArrayPoolStreamBuffer<byte>(ArrayPool<byte>.Shared, bufferSize);
+
+ ///<inheritdoc/>
+ public override async Task<string?> ReadLineAsync()
+ {
+ //If buffered data is available, check for line termination
+ if (Available > 0)
+ {
+ //Get current buffer window
+ Memory<byte> buffered = _buffer.AccumulatedBuffer;
+ //search for line termination in current buffer
+ int term = buffered.IndexOf(LineTermination);
+ //Termination found in buffer window
+ if (term > -1)
+ {
+ //Capture the line from the begining of the window to the termination
+ Memory<byte> line = buffered[..term];
+ //Shift the window to the end of the line (excluding the termination)
+ _buffer.AdvanceStart(term + LineTermination.Length);
+ //Decode the line to a string
+ return Encoding.GetString(line.Span);
+ }
+ //Termination not found
+ }
+ //Compact the buffer window and see if space is avialble to buffer more data
+ if (_buffer.CompactBufferWindow())
+ {
+ //There is room, so buffer more data
+ await _buffer.AccumulateDataAsync(BaseStream, CancellationToken.None);
+ //Check again to see if more data is buffered
+ if (Available <= 0)
+ {
+ //No string found
+ return null;
+ }
+ //Get current buffer window
+ Memory<byte> buffered = _buffer.AccumulatedBuffer;
+ //search for line termination in current buffer
+ int term = buffered.IndexOf(LineTermination);
+ //Termination found in buffer window
+ if (term > -1)
+ {
+ //Capture the line from the begining of the window to the termination
+ Memory<byte> line = buffered[..term];
+ //Shift the window to the end of the line (excluding the termination)
+ _buffer.AdvanceStart(term + LineTermination.Length);
+ //Decode the line to a string
+ return Encoding.GetString(line.Span);
+ }
+ }
+ //Termination not found within the entire buffer, so buffer space has been exhausted
+
+ //OOM is raised in the TextReader base class, the standard is preserved
+#pragma warning disable CA2201 // Do not raise reserved exception types
+ throw new OutOfMemoryException("A line termination was not found within the buffer");
+#pragma warning restore CA2201 // Do not raise reserved exception types
+ }
+
+ ///<inheritdoc/>
+ public override int Read(char[] buffer, int index, int count) => Read(buffer.AsSpan(index, count));
+ ///<inheritdoc/>
+ public override int Read(Span<char> buffer)
+ {
+ if (Available <= 0)
+ {
+ return 0;
+ }
+ //Get current buffer window
+ Span<byte> buffered = _buffer.Accumulated;
+ //Convert all avialable data
+ int encoded = Encoding.GetChars(buffered, buffer);
+ //Shift buffer window to the end of the converted data
+ _buffer.AdvanceStart(encoded);
+ //return the number of chars written
+ return Encoding.GetCharCount(buffered);
+ }
+ ///<inheritdoc/>
+ public override void Close() => _buffer.Close();
+ ///<inheritdoc/>
+ protected override void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ Close();
+ disposedValue = true;
+ }
+ base.Dispose(disposing);
+ }
+
+ /// <summary>
+ /// Resets the internal buffer window
+ /// </summary>
+ protected void ClearBuffer()
+ {
+ _buffer.Reset();
+ }
+
+ void IVnTextReader.Advance(int count) => _buffer.AdvanceStart(count);
+ void IVnTextReader.FillBuffer() => _buffer.AccumulateData(BaseStream);
+ ERRNO IVnTextReader.CompactBufferWindow() => _buffer.CompactBufferWindow();
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/VnStreamWriter.cs b/lib/Utils/src/IO/VnStreamWriter.cs
new file mode 100644
index 0000000..f875932
--- /dev/null
+++ b/lib/Utils/src/IO/VnStreamWriter.cs
@@ -0,0 +1,292 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: VnStreamWriter.cs
+*
+* VnStreamWriter.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Text;
+using System.Buffers;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils.Memory;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Provides a memory optimized <see cref="TextWriter"/> implementation. Optimized for writing
+ /// to network streams
+ /// </summary>
+ public class VnStreamWriter : TextWriter
+ {
+ private readonly Encoder Enc;
+
+ private readonly ISlindingWindowBuffer<byte> _buffer;
+
+ private bool closed;
+
+ /// <summary>
+ /// Gets the underlying stream that interfaces with the backing store
+ /// </summary>
+ public virtual Stream BaseStream { get; }
+ ///<inheritdoc/>
+ public override Encoding Encoding { get; }
+
+ /// <summary>
+ /// Line termination to use when writing lines to the output
+ /// </summary>
+ public ReadOnlyMemory<byte> LineTermination { get; set; }
+ ///<inheritdoc/>
+ public override string NewLine
+ {
+ get => Encoding.GetString(LineTermination.Span);
+ set => LineTermination = Encoding.GetBytes(value);
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="VnStreamWriter"/> that writes formatted data
+ /// to the specified base stream
+ /// </summary>
+ /// <param name="baseStream">The stream to write data to</param>
+ /// <param name="encoding">The <see cref="Encoding"/> to use when writing data</param>
+ /// <param name="bufferSize">The size of the internal buffer used to buffer binary data before writing to the base stream</param>
+ public VnStreamWriter(Stream baseStream, Encoding encoding, int bufferSize = 1024)
+ {
+ //Store base stream
+ BaseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
+ Encoding = encoding ?? throw new ArgumentNullException(nameof(encoding));
+ //Get an encoder
+ Enc = encoding.GetEncoder();
+ _buffer = InitializeBuffer(bufferSize);
+ }
+
+ /// <summary>
+ /// Invoked by the constuctor method to allocte the internal buffer with the specified buffer size.
+ /// </summary>
+ /// <param name="bufferSize">The requested size of the buffer to alloc</param>
+ /// <remarks>By default requests the buffer from the <see cref="MemoryPool{T}.Shared"/> instance</remarks>
+ protected virtual ISlindingWindowBuffer<byte> InitializeBuffer(int bufferSize) => new ArrayPoolStreamBuffer<byte>(ArrayPool<byte>.Shared, bufferSize);
+ ///<inheritdoc/>
+ public void Write(byte value)
+ {
+ //See if there is room in the binary buffer
+ if (_buffer.AccumulatedSize == 0)
+ {
+ //There is not enough room to store the single byte
+ Flush();
+ }
+ //Store at the end of the window
+ _buffer.Append(value);
+ }
+ ///<inheritdoc/>
+ public override void Write(char value)
+ {
+ ReadOnlySpan<char> tbuf = MemoryMarshal.CreateSpan(ref value, 0x01);
+ Write(tbuf);
+ }
+ ///<inheritdoc/>
+ public override void Write(object? value) => Write(value?.ToString());
+ ///<inheritdoc/>
+ public override void Write(string? value) => Write(value.AsSpan());
+ ///<inheritdoc/>
+ public override void Write(ReadOnlySpan<char> buffer)
+ {
+ Check();
+
+ ForwardOnlyReader<char> reader = new(buffer);
+
+ //Create a variable for a character buffer window
+ bool completed;
+ do
+ {
+ //Get an available buffer window to store characters in and convert the characters to binary
+ Enc.Convert(reader.Window, _buffer.Remaining, true, out int charsUsed, out int bytesUsed, out completed);
+ //Update byte position
+ _buffer.Advance(bytesUsed);
+ //Update char position
+ reader.Advance(charsUsed);
+
+ //Converting did not complete because the buffer was too small
+ if (!completed || reader.WindowSize == 0)
+ {
+ //Flush the buffer and continue
+ Flush();
+ }
+
+ } while (!completed);
+ //Reset the encoder
+ Enc.Reset();
+ }
+ ///<inheritdoc/>
+ public override async Task WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = default)
+ {
+ Check();
+ //Create a variable for a character buffer window
+ bool completed;
+ ForwardOnlyMemoryReader<char> reader = new(buffer);
+ do
+ {
+ //Get an available buffer window to store characters in and convert the characters to binary
+ Enc.Convert(reader.Window.Span, _buffer.Remaining, true, out int charsUsed, out int bytesUsed, out completed);
+ //Update byte position
+ _buffer.Advance(bytesUsed);
+ //Update char position
+ reader.Advance(charsUsed);
+ //Converting did not complete because the buffer was too small
+ if (!completed || reader.WindowSize == 0)
+ {
+ //Flush the buffer and continue
+ await FlushWriterAsync(cancellationToken);
+ }
+ } while (!completed);
+ //Reset the encoder
+ Enc.Reset();
+ }
+
+ ///<inheritdoc/>
+ public override void WriteLine()
+ {
+ Check();
+ //See if there is room in the binary buffer
+ if (_buffer.RemainingSize < LineTermination.Length)
+ {
+ //There is not enough room to store the termination, so we need to flush the buffer
+ Flush();
+ }
+ _buffer.Append(LineTermination.Span);
+ }
+ ///<inheritdoc/>
+ public override void WriteLine(object? value) => WriteLine(value?.ToString());
+ ///<inheritdoc/>
+ public override void WriteLine(string? value) => WriteLine(value.AsSpan());
+ ///<inheritdoc/>
+ public override void WriteLine(ReadOnlySpan<char> buffer)
+ {
+ //Write the value itself
+ Write(buffer);
+ //Write the line termination
+ WriteLine();
+ }
+
+ ///<inheritdoc/>
+ ///<exception cref="ObjectDisposedException"></exception>
+ public override void Flush()
+ {
+ Check();
+ //If data is available to be written, write it to the base stream
+ if (_buffer.AccumulatedSize > 0)
+ {
+ //Write all buffered data to stream
+ BaseStream.Write(_buffer.Accumulated);
+ //Reset the buffer
+ _buffer.Reset();
+ }
+ }
+ /// <summary>
+ /// Asynchronously flushes the internal buffers to the <see cref="BaseStream"/>, and resets the internal buffer state
+ /// </summary>
+ /// <returns>A <see cref="ValueTask"/> that represents the asynchronous flush operation</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public async ValueTask FlushWriterAsync(CancellationToken cancellationToken = default)
+ {
+ Check();
+ if (_buffer.AccumulatedSize > 0)
+ {
+ //Flush current window to the stream
+ await BaseStream.WriteAsync(_buffer.AccumulatedBuffer, cancellationToken);
+ //Reset the buffer
+ _buffer.Reset();
+ }
+ }
+
+ ///<inheritdoc/>
+ public override Task FlushAsync() => FlushWriterAsync().AsTask();
+
+ /// <summary>
+ /// Resets internal properies for resuse
+ /// </summary>
+ protected void Reset()
+ {
+ _buffer.Reset();
+ Enc.Reset();
+ }
+ ///<inheritdoc/>
+ public override void Close()
+ {
+ //Only invoke close once
+ if (closed)
+ {
+ return;
+ }
+ try
+ {
+ Flush();
+ }
+ finally
+ {
+ //Release the memory handle if its set
+ _buffer.Close();
+ //Set closed flag
+ closed = true;
+ }
+ }
+ ///<inheritdoc/>
+ protected override void Dispose(bool disposing)
+ {
+ Close();
+ base.Dispose(disposing);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void Check()
+ {
+ if (closed)
+ {
+ throw new ObjectDisposedException("The stream is closed");
+ }
+ }
+ ///<inheritdoc/>
+ public override async ValueTask DisposeAsync()
+ {
+ //Only invoke close once
+ if (closed)
+ {
+ return;
+ }
+ try
+ {
+ await FlushWriterAsync();
+ }
+ finally
+ {
+ //Set closed flag
+ closed = true;
+ //Release the memory handle if its set
+ _buffer.Close();
+ }
+ GC.SuppressFinalize(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/VnTextReaderExtensions.cs b/lib/Utils/src/IO/VnTextReaderExtensions.cs
new file mode 100644
index 0000000..119461b
--- /dev/null
+++ b/lib/Utils/src/IO/VnTextReaderExtensions.cs
@@ -0,0 +1,223 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: VnTextReaderExtensions.cs
+*
+* VnTextReaderExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// Extension methods to help reuse code for used TextReader implementations
+ /// </summary>
+ public static class VnTextReaderExtensions
+ {
+ public const int E_BUFFER_TOO_SMALL = -1;
+
+
+ /*
+ * Generic extensions provide constained compiler method invocation
+ * for structs the implement the IVNtextReader
+ */
+
+ /// <summary>
+ /// Attempts to read a line from the stream and store it in the specified buffer
+ /// </summary>
+ /// <param name="reader"></param>
+ /// <param name="charBuffer">The character buffer to write data to</param>
+ /// <returns>Returns the number of bytes read, <see cref="E_BUFFER_TOO_SMALL"/>
+ /// if the buffer was not large enough, 0 if no data was available</returns>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <remarks>Allows reading lines of data from the stream without allocations</remarks>
+ public static ERRNO ReadLine<T>(this ref T reader, Span<char> charBuffer) where T:struct, IVnTextReader
+ {
+ return readLine(ref reader, charBuffer);
+ }
+ /// <summary>
+ /// Attempts to read a line from the stream and store it in the specified buffer
+ /// </summary>
+ /// <param name="reader"></param>
+ /// <param name="charBuffer">The character buffer to write data to</param>
+ /// <returns>Returns the number of bytes read, <see cref="E_BUFFER_TOO_SMALL"/>
+ /// if the buffer was not large enough, 0 if no data was available</returns>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <remarks>Allows reading lines of data from the stream without allocations</remarks>
+ public static ERRNO ReadLine<T>(this T reader, Span<char> charBuffer) where T : class, IVnTextReader
+ {
+ return readLine(ref reader, charBuffer);
+ }
+
+ /// <summary>
+ /// Fill a buffer with reamining buffered data
+ /// </summary>
+ /// <param name="reader"></param>
+ /// <param name="buffer">Buffer to copy data to</param>
+ /// <param name="offset">Offset in buffer to begin writing</param>
+ /// <param name="count">Number of bytes to read</param>
+ /// <returns>The number of bytes copied to the input buffer</returns>
+ public static int ReadRemaining<T>(this ref T reader, byte[] buffer, int offset, int count) where T : struct, IVnTextReader
+ {
+ return reader.ReadRemaining(buffer.AsSpan(offset, count));
+ }
+ /// <summary>
+ /// Fill a buffer with reamining buffered data
+ /// </summary>
+ /// <param name="reader"></param>
+ /// <param name="buffer">Buffer to copy data to</param>
+ /// <param name="offset">Offset in buffer to begin writing</param>
+ /// <param name="count">Number of bytes to read</param>
+ /// <returns>The number of bytes copied to the input buffer</returns>
+ public static int ReadRemaining<T>(this T reader, byte[] buffer, int offset, int count) where T : class, IVnTextReader
+ {
+ return reader.ReadRemaining(buffer.AsSpan(offset, count));
+ }
+
+ /// <summary>
+ /// Fill a buffer with reamining buffered data, up to
+ /// the size of the supplied buffer
+ /// </summary>
+ /// <param name="reader"></param>
+ /// <param name="buffer">Buffer to copy data to</param>
+ /// <returns>The number of bytes copied to the input buffer</returns>
+ /// <remarks>You should use the <see cref="IVnTextReader.Available"/> property to know how much remaining data is buffered</remarks>
+ public static int ReadRemaining<T>(this ref T reader, Span<byte> buffer) where T : struct, IVnTextReader
+ {
+ return readRemaining(ref reader, buffer);
+ }
+ /// <summary>
+ /// Fill a buffer with reamining buffered data, up to
+ /// the size of the supplied buffer
+ /// </summary>
+ /// <param name="reader"></param>
+ /// <param name="buffer">Buffer to copy data to</param>
+ /// <returns>The number of bytes copied to the input buffer</returns>
+ /// <remarks>You should use the <see cref="IVnTextReader.Available"/> property to know how much remaining data is buffered</remarks>
+ public static int ReadRemaining<T>(this T reader, Span<byte> buffer) where T : class, IVnTextReader
+ {
+ return readRemaining(ref reader, buffer);
+ }
+
+ private static ERRNO readLine<T>(ref T reader, Span<char> chars) where T: IVnTextReader
+ {
+ /*
+ * I am aware of a potential bug, the line decoding process
+ * shifts the interal buffer by the exact number of bytes to
+ * the end of the line, without considering if the decoder failed
+ * to properly decode the entire line.
+ *
+ * I dont expect this to be an issue unless there is a bug within the specified
+ * encoder implementation
+ */
+ ReadOnlySpan<byte> LineTermination = reader.LineTermination.Span;
+ //If buffered data is available, check for line termination
+ if (reader.Available > 0)
+ {
+ //Get current buffer window
+ ReadOnlySpan<byte> bytes = reader.BufferedDataWindow;
+ //search for line termination in current buffer
+ int term = bytes.IndexOf(LineTermination);
+ //Termination found in buffer window
+ if (term > -1)
+ {
+ //Capture the line from the begining of the window to the termination
+ ReadOnlySpan<byte> line = bytes[..term];
+ //Get the number ot chars
+ int charCount = reader.Encoding.GetCharCount(line);
+ //See if the buffer is large enough
+ if (bytes.Length < charCount)
+ {
+ return E_BUFFER_TOO_SMALL;
+ }
+ //Use the decoder to convert the data
+ _ = reader.Encoding.GetChars(line, chars);
+ //Shift the window to the end of the line (excluding the termination, regardless of the conversion result)
+ reader.Advance(term + LineTermination.Length);
+ //Return the number of characters
+ return charCount;
+ }
+ //Termination not found but there may be more data waiting
+ }
+ //Compact the buffer window and make sure it was compacted so there is room to fill the buffer
+ if (reader.CompactBufferWindow())
+ {
+ //There is room, so buffer more data
+ reader.FillBuffer();
+ //Check again to see if more data is buffered
+ if (reader.Available <= 0)
+ {
+ //No data avialable
+ return 0;
+ }
+ //Get current buffer window
+ ReadOnlySpan<byte> bytes = reader.BufferedDataWindow;
+ //search for line termination in current buffer
+ int term = bytes.IndexOf(LineTermination);
+ //Termination found in buffer window
+ if (term > -1)
+ {
+ //Capture the line from the begining of the window to the termination
+ ReadOnlySpan<byte> line = bytes[..term];
+ //Get the number ot chars
+ int charCount = reader.Encoding.GetCharCount(line);
+ //See if the buffer is large enough
+ if (bytes.Length < charCount)
+ {
+ return E_BUFFER_TOO_SMALL;
+ }
+ //Use the decoder to convert the data
+ _ = reader.Encoding.GetChars(line, chars);
+ //Shift the window to the end of the line (excluding the termination, regardless of the conversion result)
+ reader.Advance(term + LineTermination.Length);
+ //Return the number of characters
+ return charCount;
+ }
+ }
+
+ //Termination not found within the entire buffer, so buffer space has been exhausted
+
+ //Supress as this response is expected when the buffer is exhausted,
+#pragma warning disable CA2201 // Do not raise reserved exception types
+ throw new OutOfMemoryException("The line was not found within the current buffer, cannot continue");
+#pragma warning restore CA2201 // Do not raise reserved exception types
+ }
+
+ private static int readRemaining<T>(ref T reader, Span<byte> buffer) where T: IVnTextReader
+ {
+ //guard for empty buffer
+ if (buffer.Length == 0 || reader.Available == 0)
+ {
+ return 0;
+ }
+ //get the remaining bytes in the reader
+ Span<byte> remaining = reader.BufferedDataWindow;
+ //Calculate the number of bytes to copy
+ int canCopy = Math.Min(remaining.Length, buffer.Length);
+ //Copy remaining bytes to buffer
+ remaining[..canCopy].CopyTo(buffer);
+ //Shift the window by the number of bytes copied
+ reader.Advance(canCopy);
+ return canCopy;
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/IO/WriteOnlyBufferedStream.cs b/lib/Utils/src/IO/WriteOnlyBufferedStream.cs
new file mode 100644
index 0000000..5e7faa1
--- /dev/null
+++ b/lib/Utils/src/IO/WriteOnlyBufferedStream.cs
@@ -0,0 +1,255 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: WriteOnlyBufferedStream.cs
+*
+* WriteOnlyBufferedStream.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Buffers;
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Memory;
+
+namespace VNLib.Utils.IO
+{
+ /// <summary>
+ /// A basic accumulator style write buffered stream
+ /// </summary>
+ public class WriteOnlyBufferedStream : Stream
+ {
+ private readonly ISlindingWindowBuffer<byte> _buffer;
+ private readonly bool LeaveOpen;
+
+ /// <summary>
+ /// Gets the underlying stream that interfaces with the backing store
+ /// </summary>
+ public Stream BaseStream { get; init; }
+
+ /// <summary>
+ /// Initalizes a new <see cref="WriteOnlyBufferedStream"/> using the
+ /// specified backing stream, using the specified buffer size, and
+ /// optionally leaves the stream open
+ /// </summary>
+ /// <param name="baseStream">The backing stream to write buffered data to</param>
+ /// <param name="bufferSize">The size of the internal buffer</param>
+ /// <param name="leaveOpen">A value indicating of the stream should be left open when the buffered stream is closed</param>
+ public WriteOnlyBufferedStream(Stream baseStream, int bufferSize, bool leaveOpen = false)
+ {
+ BaseStream = baseStream;
+ //Create buffer
+ _buffer = InitializeBuffer(bufferSize);
+ LeaveOpen = leaveOpen;
+ }
+ /// <summary>
+ /// Invoked by the constuctor method to allocte the internal buffer with the specified buffer size.
+ /// </summary>
+ /// <param name="bufferSize">The requested size of the buffer to alloc</param>
+ /// <remarks>By default requests the buffer from the <see cref="ArrayPool{T}.Shared"/> instance</remarks>
+ protected virtual ISlindingWindowBuffer<byte> InitializeBuffer(int bufferSize)
+ {
+ return new ArrayPoolStreamBuffer<byte>(ArrayPool<byte>.Shared, bufferSize);
+ }
+
+ ///<inheritdoc/>
+ public override void Close()
+ {
+ try
+ {
+ //Make sure the buffer is empty
+ WriteBuffer();
+
+ if (!LeaveOpen)
+ {
+ //Dispose stream
+ BaseStream.Dispose();
+ }
+ }
+ finally
+ {
+ _buffer.Close();
+ }
+ }
+ ///<inheritdoc/>
+ public override async ValueTask DisposeAsync()
+ {
+ try
+ {
+ if (_buffer.AccumulatedSize > 0)
+ {
+ await WriteBufferAsync(CancellationToken.None);
+ }
+
+ if (!LeaveOpen)
+ {
+ //Dispose stream
+ await BaseStream.DisposeAsync();
+ }
+
+ GC.SuppressFinalize(this);
+ }
+ finally
+ {
+ _buffer.Close();
+ }
+ }
+
+ ///<inheritdoc/>
+ public override void Flush() => WriteBuffer();
+ ///<inheritdoc/>
+ public override Task FlushAsync(CancellationToken cancellationToken) => WriteBufferAsync(cancellationToken).AsTask();
+
+ private void WriteBuffer()
+ {
+ //Only if data is available to write
+ if (_buffer.AccumulatedSize > 0)
+ {
+ //Write data to stream
+ BaseStream.Write(_buffer.Accumulated);
+ //Reset position
+ _buffer.Reset();
+ }
+ }
+
+ private async ValueTask WriteBufferAsync(CancellationToken token = default)
+ {
+ if(_buffer.AccumulatedSize > 0)
+ {
+ await BaseStream.WriteAsync(_buffer.AccumulatedBuffer, token);
+ _buffer.Reset();
+ }
+ }
+ ///<inheritdoc/>
+ public override void Write(byte[] buffer, int offset, int count) => Write(buffer.AsSpan(offset, count));
+
+ public override void Write(ReadOnlySpan<byte> buffer)
+ {
+ ForwardOnlyReader<byte> reader = new(buffer);
+ //Attempt to buffer/flush data until all data is sent
+ do
+ {
+ //Try to buffer as much as possible
+ ERRNO buffered = _buffer.TryAccumulate(reader.Window);
+
+ if(buffered < reader.WindowSize)
+ {
+ //Buffer is full and needs to be flushed
+ WriteBuffer();
+ //Advance reader and continue to buffer
+ reader.Advance(buffered);
+ continue;
+ }
+
+ break;
+ }
+ while (true);
+ }
+
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
+ }
+
+ public async override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ ForwardOnlyMemoryReader<byte> reader = new(buffer);
+ //Attempt to buffer/flush data until all data is sent
+ do
+ {
+ //Try to buffer as much as possible
+ ERRNO buffered = _buffer.TryAccumulate(reader.Window.Span);
+
+ if (buffered < reader.WindowSize)
+ {
+ //Buffer is full and needs to be flushed
+ await WriteBufferAsync(cancellationToken);
+ //Advance reader and continue to buffer
+ reader.Advance(buffered);
+ continue;
+ }
+
+ break;
+ }
+ while (true);
+ }
+
+
+ /// <summary>
+ /// Always false
+ /// </summary>
+ public override bool CanRead => false;
+ /// <summary>
+ /// Always returns false
+ /// </summary>
+ public override bool CanSeek => false;
+ /// <summary>
+ /// Always true
+ /// </summary>
+ public override bool CanWrite => true;
+ /// <summary>
+ /// Returns the size of the underlying buffer
+ /// </summary>
+ public override long Length => _buffer.AccumulatedSize;
+ /// <summary>
+ /// Always throws <see cref="NotSupportedException"/>
+ /// </summary>
+ /// <exception cref="NotSupportedException"></exception>
+ public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
+ /// <summary>
+ /// Always throws <see cref="NotSupportedException"/>
+ /// </summary>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <returns></returns>
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException("This stream is not readable");
+ }
+
+ /// <summary>
+ /// Always throws <see cref="NotSupportedException"/>
+ /// </summary>
+ /// <exception cref="NotSupportedException"></exception>
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Always throws <see cref="NotSupportedException"/>
+ /// </summary>
+ /// <exception cref="NotSupportedException"></exception>
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/lib/Utils/src/IObjectStorage.cs b/lib/Utils/src/IObjectStorage.cs
new file mode 100644
index 0000000..5c99cd8
--- /dev/null
+++ b/lib/Utils/src/IObjectStorage.cs
@@ -0,0 +1,48 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IObjectStorage.cs
+*
+* IObjectStorage.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Utils
+{
+ /// <summary>
+ /// This object will provide methods for storing and retreiving objects by key-value pairing
+ /// </summary>
+ public interface IObjectStorage
+ {
+ /// <summary>
+ /// Attempts to retrieve the specified object from storage
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="key">Key for storage</param>
+ /// <returns>The object in storage, or T.default if object is not found</returns>
+ public T GetObject<T>(string key);
+
+ /// <summary>
+ /// Stores the specified object with the specified key
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="key">Key paired with object</param>
+ /// <param name="obj">Object to store</param>
+ public void SetObject<T>(string key, T obj);
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Logging/ILogProvider.cs b/lib/Utils/src/Logging/ILogProvider.cs
new file mode 100644
index 0000000..55dbd6f
--- /dev/null
+++ b/lib/Utils/src/Logging/ILogProvider.cs
@@ -0,0 +1,79 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ILogProvider.cs
+*
+* ILogProvider.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Logging
+{
+ /// <summary>
+ /// Self-contained logging interface that allows for applications events to be written to an
+ /// output source
+ /// </summary>
+ public interface ILogProvider
+ {
+ /// <summary>
+ /// Flushes any buffers to the output source
+ /// </summary>
+ abstract void Flush();
+
+ /// <summary>
+ /// Writes the string to the log with the specified priority log level
+ /// </summary>
+ /// <param name="level">The log priority level</param>
+ /// <param name="value">The message to print</param>
+ void Write(LogLevel level, string value);
+ /// <summary>
+ /// Writes the exception and optional string to the log with the specified priority log level
+ /// </summary>
+ /// <param name="level">The log priority level</param>
+ /// <param name="exception">An exception object to write</param>
+ /// <param name="value">The message to print</param>
+ void Write(LogLevel level, Exception exception, string value = "");
+ /// <summary>
+ /// Writes the template string and params arguments to the log with the specified priority log level
+ /// </summary>
+ /// <param name="level">The log priority level</param>
+ /// <param name="value">The log template string</param>
+ /// <param name="args">Variable length array of objects to log with the specified templatre</param>
+ void Write(LogLevel level, string value, params object?[] args);
+ /// <summary>
+ /// Writes the template string and params arguments to the log with the specified priority log level
+ /// </summary>
+ /// <param name="level">The log priority level</param>
+ /// <param name="value">The log template string</param>
+ /// <param name="args">Variable length array of objects to log with the specified templatre</param>
+ void Write(LogLevel level, string value, params ValueType[] args);
+
+ /// <summary>
+ /// Gets the underlying log source
+ /// </summary>
+ /// <returns>The underlying log source</returns>
+ object GetLogProvider();
+ /// <summary>
+ /// Gets the underlying log source
+ /// </summary>
+ /// <returns>The underlying log source</returns>
+ public virtual T GetLogProvider<T>() => (T)GetLogProvider();
+ }
+}
diff --git a/lib/Utils/src/Logging/LogLevel.cs b/lib/Utils/src/Logging/LogLevel.cs
new file mode 100644
index 0000000..1851c26
--- /dev/null
+++ b/lib/Utils/src/Logging/LogLevel.cs
@@ -0,0 +1,33 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: LogLevel.cs
+*
+* LogLevel.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Logging
+{
+ public enum LogLevel
+ {
+ Verbose, Debug, Information, Warning, Error, Fatal
+ }
+}
diff --git a/lib/Utils/src/Logging/LoggerExtensions.cs b/lib/Utils/src/Logging/LoggerExtensions.cs
new file mode 100644
index 0000000..cd314ed
--- /dev/null
+++ b/lib/Utils/src/Logging/LoggerExtensions.cs
@@ -0,0 +1,60 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: LoggerExtensions.cs
+*
+* LoggerExtensions.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+#pragma warning disable CA1062 // Validate arguments of public methods
+
+using System;
+
+namespace VNLib.Utils.Logging
+{
+ /// <summary>
+ /// Extension helper methods for writing logs to a <see cref="ILogProvider"/>
+ /// </summary>
+ public static class LoggerExtensions
+ {
+ public static void Debug(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Debug, exp, value);
+ public static void Debug(this ILogProvider log, string value) => log.Write(LogLevel.Debug, value);
+ public static void Debug(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Debug, format, args);
+ public static void Debug(this ILogProvider log, string format, params ValueType[] args) => log.Write(LogLevel.Debug, format, args);
+ public static void Error(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Error, exp, value);
+ public static void Error(this ILogProvider log, string value) => log.Write(LogLevel.Error, value);
+ public static void Error(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Error, format, args);
+ public static void Fatal(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Fatal, exp, value);
+ public static void Fatal(this ILogProvider log, string value) => log.Write(LogLevel.Fatal, value);
+ public static void Fatal(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Fatal, format, args);
+ public static void Fatal(this ILogProvider log, string format, params ValueType[] args) => log.Write(LogLevel.Fatal, format, args);
+ public static void Information(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Information, exp, value);
+ public static void Information(this ILogProvider log, string value) => log.Write(LogLevel.Information, value);
+ public static void Information(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Information, format, args);
+ public static void Information(this ILogProvider log, string format, params ValueType[] args) => log.Write(LogLevel.Information, format, args);
+ public static void Verbose(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Verbose, exp, value);
+ public static void Verbose(this ILogProvider log, string value) => log.Write(LogLevel.Verbose, value);
+ public static void Verbose(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Verbose, format, args);
+ public static void Verbose(this ILogProvider log, string format, params ValueType[] args) => log.Write(LogLevel.Verbose, format, args);
+ public static void Warn(this ILogProvider log, Exception exp, string value = "") => log.Write(LogLevel.Warning, exp, value);
+ public static void Warn(this ILogProvider log, string value) => log.Write(LogLevel.Warning, value);
+ public static void Warn(this ILogProvider log, string format, params object?[] args) => log.Write(LogLevel.Warning, format, args);
+ public static void Warn(this ILogProvider log, string format, params ValueType[] args) => log.Write(LogLevel.Warning, format, args);
+ }
+}
diff --git a/lib/Utils/src/Memory/Caching/ICacheHolder.cs b/lib/Utils/src/Memory/Caching/ICacheHolder.cs
new file mode 100644
index 0000000..19eee64
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/ICacheHolder.cs
@@ -0,0 +1,45 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ICacheHolder.cs
+*
+* ICacheHolder.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory.Caching
+{
+ /// <summary>
+ /// Exposes basic control of classes that manage private caches
+ /// </summary>
+ public interface ICacheHolder
+ {
+ /// <summary>
+ /// Clears all held caches without causing application stopping effects.
+ /// </summary>
+ /// <remarks>This is a safe "light" cache clear</remarks>
+ void CacheClear();
+ /// <summary>
+ /// Performs all necessary actions to clear all held caches immediatly.
+ /// </summary>
+ /// <remarks>A "hard" cache clear/reset regardless of cost</remarks>
+ void CacheHardClear();
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/Caching/ICacheable.cs b/lib/Utils/src/Memory/Caching/ICacheable.cs
new file mode 100644
index 0000000..37575cc
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/ICacheable.cs
@@ -0,0 +1,44 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ICacheable.cs
+*
+* ICacheable.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory.Caching
+{
+ /// <summary>
+ /// Represents a cacheable entity with an expiration
+ /// </summary>
+ public interface ICacheable : IEquatable<ICacheable>
+ {
+ /// <summary>
+ /// A <see cref="DateTime"/> value that the entry is no longer valid
+ /// </summary>
+ DateTime Expires { get; set; }
+
+ /// <summary>
+ /// Invoked when a collection occurs
+ /// </summary>
+ void Evicted();
+ }
+}
diff --git a/lib/Utils/src/Memory/Caching/IObjectRental.cs b/lib/Utils/src/Memory/Caching/IObjectRental.cs
new file mode 100644
index 0000000..d9489f4
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/IObjectRental.cs
@@ -0,0 +1,47 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IObjectRental.cs
+*
+* IObjectRental.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Utils.Memory.Caching
+{
+
+ /// <summary>
+ /// A thread safe store for reusing CLR managed objects
+ /// </summary>
+ /// <typeparam name="T">The reusable object class</typeparam>
+ public interface IObjectRental<T> where T: class
+ {
+ /// <summary>
+ /// Gets an object from the store, or creates a new one if none are available
+ /// </summary>
+ /// <returns>An instance of <typeparamref name="T"/> from the store if available or a new instance if none were available</returns>
+ T Rent();
+
+ /// <summary>
+ /// Returns a rented object back to the rental store for reuse
+ /// </summary>
+ /// <param name="item">The previously rented item</param>
+ void Return(T item);
+ }
+
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/Caching/IReusable.cs b/lib/Utils/src/Memory/Caching/IReusable.cs
new file mode 100644
index 0000000..618878f
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/IReusable.cs
@@ -0,0 +1,42 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IReusable.cs
+*
+* IReusable.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Utils.Memory.Caching
+{
+ /// <summary>
+ /// Allows for use within a <see cref="ReusableStore{T}"/>, this object is intended to be reused heavily
+ /// </summary>
+ public interface IReusable
+ {
+ /// <summary>
+ /// The instance should prepare itself for use (or re-use)
+ /// </summary>
+ void Prepare();
+ /// <summary>
+ /// The intance is being returned and should determine if it's state is reusabled
+ /// </summary>
+ /// <returns>true if the instance can/should be reused, false if it should not be reused</returns>
+ bool Release();
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/Caching/LRUCache.cs b/lib/Utils/src/Memory/Caching/LRUCache.cs
new file mode 100644
index 0000000..7e96e0a
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/LRUCache.cs
@@ -0,0 +1,127 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: LRUCache.cs
+*
+* LRUCache.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace VNLib.Utils.Memory.Caching
+{
+ /// <summary>
+ /// A base class for a Least Recently Used cache
+ /// </summary>
+ /// <typeparam name="TKey">The key for O(1) lookups</typeparam>
+ /// <typeparam name="TValue">The value to store within cache</typeparam>
+ public abstract class LRUCache<TKey, TValue> : LRUDataStore<TKey, TValue> where TKey : notnull
+ {
+ ///<inheritdoc/>
+ protected LRUCache()
+ {}
+ ///<inheritdoc/>
+ protected LRUCache(int initialCapacity) : base(initialCapacity)
+ {}
+ ///<inheritdoc/>
+ protected LRUCache(IEqualityComparer<TKey> keyComparer) : base(keyComparer)
+ {}
+ ///<inheritdoc/>
+ protected LRUCache(int initialCapacity, IEqualityComparer<TKey> keyComparer) : base(initialCapacity, keyComparer)
+ {}
+
+ /// <summary>
+ /// The maximum number of items to store in LRU cache
+ /// </summary>
+ protected abstract int MaxCapacity { get; }
+
+ /// <summary>
+ /// Adds a new record to the LRU cache
+ /// </summary>
+ /// <param name="item">A <see cref="KeyValuePair{TKey, TValue}"/> to add to the cache store</param>
+ public override void Add(KeyValuePair<TKey, TValue> item)
+ {
+ //See if the store is at max capacity and an item needs to be evicted
+ if(Count == MaxCapacity)
+ {
+ //A record needs to be evicted before a new record can be added
+
+ //Get the oldest node from the list to reuse its instance and remove the old value
+ LinkedListNode<KeyValuePair<TKey, TValue>> oldNode = List.First!; //not null because count is at max capacity so an item must be at the end of the list
+ //Store old node value field
+ KeyValuePair<TKey, TValue> oldRecord = oldNode.Value;
+ //Remove from lookup
+ LookupTable.Remove(oldRecord.Key);
+ //Remove the node
+ List.RemoveFirst();
+ //Reuse the old ll node
+ oldNode.Value = item;
+ //add lookup with new key
+ LookupTable.Add(item.Key, oldNode);
+ //Add to end of list
+ List.AddLast(oldNode);
+ //Invoke evicted method
+ Evicted(oldRecord);
+ }
+ else
+ {
+ //Add new item to the list
+ base.Add(item);
+ }
+ }
+ /// <summary>
+ /// Attempts to get a value by the given key.
+ /// </summary>
+ /// <param name="key">The key identifying the value to store</param>
+ /// <param name="value">The value to store</param>
+ /// <returns>A value indicating if the value was found in the store</returns>
+ public override bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value)
+ {
+ //See if the cache contains the value
+ if(base.TryGetValue(key, out value))
+ {
+ //Cache hit
+ return true;
+ }
+ //Cache miss
+ if(CacheMiss(key, out value))
+ {
+ //Lookup hit
+ //Add the record to the store (eviction will happen as necessary
+ Add(key, value);
+ return true;
+ }
+ //Record does not exist
+ return false;
+ }
+ /// <summary>
+ /// Invoked when a record is evicted from the cache
+ /// </summary>
+ /// <param name="evicted">The record that is being evicted</param>
+ protected abstract void Evicted(KeyValuePair<TKey, TValue> evicted);
+ /// <summary>
+ /// Invoked when an entry was requested and was not found in cache.
+ /// </summary>
+ /// <param name="key">The key identifying the record to lookup</param>
+ /// <param name="value">The found value matching the key</param>
+ /// <returns>A value indicating if the record was found</returns>
+ protected abstract bool CacheMiss(TKey key, [NotNullWhen(true)] out TValue? value);
+ }
+}
diff --git a/lib/Utils/src/Memory/Caching/LRUDataStore.cs b/lib/Utils/src/Memory/Caching/LRUDataStore.cs
new file mode 100644
index 0000000..f564fcc
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/LRUDataStore.cs
@@ -0,0 +1,232 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: LRUDataStore.cs
+*
+* LRUDataStore.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace VNLib.Utils.Memory.Caching
+{
+ /// <summary>
+ /// A Least Recently Used store base class for E2E O(1) operations
+ /// </summary>
+ /// <typeparam name="TKey">A key used for O(1) lookups</typeparam>
+ /// <typeparam name="TValue">A value to store</typeparam>
+ public abstract class LRUDataStore<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IReadOnlyCollection<TValue>, IEnumerable<KeyValuePair<TKey, TValue>>
+ where TKey: notnull
+ {
+ /// <summary>
+ /// A lookup table that provides O(1) access times for key-value lookups
+ /// </summary>
+ protected Dictionary<TKey, LinkedListNode<KeyValuePair<TKey, TValue>>> LookupTable { get; }
+ /// <summary>
+ /// A linked list that tracks the least recently used item.
+ /// New items (or recently access items) are moved to the end of the list.
+ /// The head contains the least recently used item
+ /// </summary>
+ protected LinkedList<KeyValuePair<TKey, TValue>> List { get; }
+
+ /// <summary>
+ /// Initializes an empty <see cref="LRUDataStore{TKey, TValue}"/>
+ /// </summary>
+ protected LRUDataStore()
+ {
+ LookupTable = new();
+ List = new();
+ }
+ /// <summary>
+ /// Initializes an empty <see cref="LRUDataStore{TKey, TValue}"/> and sets
+ /// the lookup table's inital capacity
+ /// </summary>
+ /// <param name="initialCapacity">LookupTable initial capacity</param>
+ protected LRUDataStore(int initialCapacity)
+ {
+ LookupTable = new(initialCapacity);
+ List = new();
+ }
+ /// <summary>
+ /// Initializes an empty <see cref="LRUDataStore{TKey, TValue}"/> and uses the
+ /// specified keycomparison
+ /// </summary>
+ /// <param name="keyComparer">A <see cref="IEqualityComparer{T}"/> used by the Lookuptable to compare keys</param>
+ protected LRUDataStore(IEqualityComparer<TKey> keyComparer)
+ {
+ LookupTable = new(keyComparer);
+ List = new();
+ }
+ /// <summary>
+ /// Initializes an empty <see cref="LRUDataStore{TKey, TValue}"/> and uses the
+ /// specified keycomparison, and sets the lookup table's initial capacity
+ /// </summary>
+ /// <param name="initialCapacity">LookupTable initial capacity</param>
+ /// <param name="keyComparer">A <see cref="IEqualityComparer{T}"/> used by the Lookuptable to compare keys</param>
+ protected LRUDataStore(int initialCapacity, IEqualityComparer<TKey> keyComparer)
+ {
+ LookupTable = new(initialCapacity, keyComparer);
+ List = new();
+ }
+
+ /// <summary>
+ /// Gets or sets a value within the LRU cache.
+ /// </summary>
+ /// <param name="key">The key identifying the value</param>
+ /// <returns>The value stored at the given key</returns>
+ /// <remarks>Items are promoted in the store when accessed</remarks>
+ public virtual TValue this[TKey key]
+ {
+ get
+ {
+ return TryGetValue(key, out TValue? value)
+ ? value
+ : throw new KeyNotFoundException("The item or its key were not found in the LRU data store");
+ }
+ set
+ {
+ //If a node by the same key in the store exists, just replace its value
+ if(LookupTable.TryGetValue(key, out LinkedListNode<KeyValuePair<TKey, TValue>>? oldNode))
+ {
+ //Remove the node before re-adding it
+ List.Remove(oldNode);
+ oldNode.Value = new KeyValuePair<TKey, TValue>(key, value);
+ //Move the item to the front of the list
+ List.AddLast(oldNode);
+ }
+ else
+ {
+ //Node does not exist yet so create new one
+ Add(key, value);
+ }
+ }
+ }
+ ///<inheritdoc/>
+ public ICollection<TKey> Keys => LookupTable.Keys;
+ ///<inheritdoc/>
+ ///<exception cref="NotImplementedException"></exception>
+ public ICollection<TValue> Values => throw new NotImplementedException();
+ IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => LookupTable.Keys;
+ IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => List.Select(static node => node.Value);
+ IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => List.Select(static node => node.Value).GetEnumerator();
+
+ /// <summary>
+ /// Gets the number of items within the LRU store
+ /// </summary>
+ public int Count => List.Count;
+ ///<inheritdoc/>
+ public abstract bool IsReadOnly { get; }
+
+ /// <summary>
+ /// Adds the specified record to the store and places it at the end of the LRU queue
+ /// </summary>
+ /// <param name="key">The key identifying the record</param>
+ /// <param name="value">The value to store at the key</param>
+ public void Add(TKey key, TValue value)
+ {
+ //Create new kvp lookup ref
+ KeyValuePair<TKey, TValue> lookupRef = new(key, value);
+ //Insert the lookup
+ Add(lookupRef);
+ }
+ ///<inheritdoc/>
+ public bool Remove(KeyValuePair<TKey, TValue> item) => Remove(item.Key);
+ ///<inheritdoc/>
+ IEnumerator IEnumerable.GetEnumerator() => List.GetEnumerator();
+ ///<inheritdoc/>
+ public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) => List.CopyTo(array, arrayIndex);
+ ///<inheritdoc/>
+ public virtual bool ContainsKey(TKey key) => LookupTable.ContainsKey(key);
+ ///<inheritdoc/>
+ public virtual IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => List.GetEnumerator();
+
+ /// <summary>
+ /// Adds the specified record to the store and places it at the end of the LRU queue
+ /// </summary>
+ /// <param name="item">The item to add</param>
+ public virtual void Add(KeyValuePair<TKey, TValue> item)
+ {
+ //Init new ll node
+ LinkedListNode<KeyValuePair<TKey, TValue>> newNode = new(item);
+ //Insert the new node
+ LookupTable.Add(item.Key, newNode);
+ //Add to the end of the linked list
+ List.AddLast(newNode);
+ }
+ /// <summary>
+ /// Removes all elements from the LRU store
+ /// </summary>
+ public virtual void Clear()
+ {
+ //Clear lists
+ LookupTable.Clear();
+ List.Clear();
+ }
+ /// <summary>
+ /// Determines if the <see cref="KeyValuePair{TKey, TValue}"/> exists in the store
+ /// </summary>
+ /// <param name="item">The record to search for</param>
+ /// <returns>True if the key was found in the store and the value equals the stored value, false otherwise</returns>
+ public virtual bool Contains(KeyValuePair<TKey, TValue> item)
+ {
+ if (LookupTable.TryGetValue(item.Key, out LinkedListNode<KeyValuePair<TKey, TValue>>? lookup))
+ {
+ return lookup.Value.Value?.Equals(item.Value) ?? false;
+ }
+ return false;
+ }
+ ///<inheritdoc/>
+ public virtual bool Remove(TKey key)
+ {
+ //Remove the item from the lookup table and if it exists, remove the node from the list
+ if(LookupTable.Remove(key, out LinkedListNode<KeyValuePair<TKey, TValue>>? node))
+ {
+ //Remove the new from the list
+ List.Remove(node);
+ return true;
+ }
+ return false;
+ }
+ /// <summary>
+ /// Tries to get a value from the store with its key. Found items are promoted
+ /// </summary>
+ /// <param name="key">The key identifying the value</param>
+ /// <param name="value">The found value</param>
+ /// <returns>A value indicating if the element was found in the store</returns>
+ public virtual bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value)
+ {
+ //Lookup the
+ if (LookupTable.TryGetValue(key, out LinkedListNode<KeyValuePair<TKey, TValue>>? val))
+ {
+ //Remove the value from the list and add it to the front of the list
+ List.Remove(val);
+ List.AddLast(val);
+ value = val.Value.Value!;
+ return true;
+ }
+ value = default;
+ return false;
+ }
+
+ }
+}
diff --git a/lib/Utils/src/Memory/Caching/ObjectRental.cs b/lib/Utils/src/Memory/Caching/ObjectRental.cs
new file mode 100644
index 0000000..22aca95
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/ObjectRental.cs
@@ -0,0 +1,236 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ObjectRental.cs
+*
+* ObjectRental.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+using System.Diagnostics;
+using System.Collections;
+using System.Collections.Generic;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.Memory.Caching
+{
+ //TODO: implement lock-free object tracking
+
+ /// <summary>
+ /// Provides concurrent storage for reusable objects to be rented and returned. This class
+ /// and its members is thread-safe
+ /// </summary>
+ /// <typeparam name="T">The data type to reuse</typeparam>
+ public class ObjectRental<T> : ObjectRental, IObjectRental<T>, ICacheHolder, IEnumerable<T> where T: class
+ {
+ /// <summary>
+ /// The initial data-structure capacity if quota is not defined
+ /// </summary>
+ public const int INITIAL_STRUCTURE_SIZE = 50;
+
+ protected readonly SemaphoreSlim StorageLock;
+ protected readonly Stack<T> Storage;
+ protected readonly HashSet<T> ContainsStore;
+
+ protected readonly Action<T>? ReturnAction;
+ protected readonly Action<T>? RentAction;
+ protected readonly Func<T> Constructor;
+ /// <summary>
+ /// Is the object type in the current store implement the Idisposable interface?
+ /// </summary>
+ protected readonly bool IsDisposableType;
+
+ /// <summary>
+ /// The maximum number of objects that will be cached.
+ /// Once this threshold has been reached, objects are
+ /// no longer stored
+ /// </summary>
+ protected readonly int QuotaLimit;
+
+#pragma warning disable CS8618 //Internal constructor does not set the constructor function
+ private ObjectRental(int quota)
+#pragma warning restore CS8618
+ {
+ //alloc new stack for rentals
+ Storage = new(Math.Max(quota, INITIAL_STRUCTURE_SIZE));
+ //Hashtable for quick lookups
+ ContainsStore = new(Math.Max(quota, INITIAL_STRUCTURE_SIZE));
+ //Semaphore slim to provide exclusive access
+ StorageLock = new SemaphoreSlim(1, 1);
+ //Store quota, if quota is -1, set to int-max to "disable quota"
+ QuotaLimit = quota == 0 ? int.MaxValue : quota;
+ //Determine if the type is disposeable and store a local value
+ IsDisposableType = typeof(IDisposable).IsAssignableFrom(typeof(T));
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="ObjectRental{T}"/> store with the rent/return callback methods
+ /// </summary>
+ /// <param name="constructor">The type initializer</param>
+ /// <param name="rentCb">The pre-retnal preperation action</param>
+ /// <param name="returnCb">The pre-return cleanup action</param>
+ /// <param name="quota">The maximum number of elements to cache in the store</param>
+ protected internal ObjectRental(Func<T> constructor, Action<T>? rentCb, Action<T>? returnCb, int quota) : this(quota)
+ {
+ this.RentAction = rentCb;
+ this.ReturnAction = returnCb;
+ this.Constructor = constructor;
+ }
+
+ /// <inheritdoc/>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public virtual T Rent()
+ {
+ Check();
+ //See if we have an available object, if not return a new one by invoking the constructor function
+ T? rental = default;
+ //Get lock
+ using (SemSlimReleaser releader = StorageLock.GetReleaser())
+ {
+ //See if the store contains an item ready to use
+ if(Storage.TryPop(out T? item))
+ {
+ rental = item;
+ //Remove the item from the hash table
+ ContainsStore.Remove(item);
+ }
+ }
+ //If no object was removed from the store, create a new one
+ rental ??= Constructor();
+ //If rental cb is defined, invoke it
+ RentAction?.Invoke(rental);
+ return rental;
+ }
+
+ /// <inheritdoc/>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public virtual void Return(T item)
+ {
+ Check();
+ //Invoke return callback if set
+ ReturnAction?.Invoke(item);
+ //Keeps track to know if the element was added or need to be cleaned up
+ bool wasAdded = false;
+ using (SemSlimReleaser releader = StorageLock.GetReleaser())
+ {
+ //Check quota limit
+ if (Storage.Count < QuotaLimit)
+ {
+ //Store item if it doesnt exist already
+ if (ContainsStore.Add(item))
+ {
+ //Store the object
+ Storage.Push(item);
+ }
+ //Set the was added flag
+ wasAdded = true;
+ }
+ }
+ if (!wasAdded && IsDisposableType)
+ {
+ //If the element was not added and is disposeable, we can dispose the element
+ (item as IDisposable)!.Dispose();
+ //Write debug message
+ Debug.WriteLine("Object rental disposed an object over quota");
+ }
+ }
+
+ /// <remarks>
+ /// NOTE: If <typeparamref name="T"/> implements <see cref="IDisposable"/>
+ /// interface, this method does nothing
+ /// </remarks>
+ /// <inheritdoc/>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public virtual void CacheClear()
+ {
+ Check();
+ //If the type is disposeable, cleaning can be a long process, so defer to hard clear
+ if (IsDisposableType)
+ {
+ return;
+ }
+ //take the semaphore
+ using SemSlimReleaser releader = StorageLock.GetReleaser();
+ //Clear stores
+ ContainsStore.Clear();
+ Storage.Clear();
+ }
+
+ /// <inheritdoc/>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public virtual void CacheHardClear()
+ {
+ Check();
+ //take the semaphore
+ using SemSlimReleaser releader = StorageLock.GetReleaser();
+ //If the type is disposable, dispose all elements before clearing storage
+ if (IsDisposableType)
+ {
+ //Dispose all elements
+ foreach (T element in Storage.ToArray())
+ {
+ (element as IDisposable)!.Dispose();
+ }
+ }
+ //Clear the storeage
+ Storage.Clear();
+ ContainsStore.Clear();
+ }
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ StorageLock.Dispose();
+ //If the element type is disposable, dispose all elements on a hard clear
+ if (IsDisposableType)
+ {
+ //Get all elements
+ foreach (T element in Storage.ToArray())
+ {
+ (element as IDisposable)!.Dispose();
+ }
+ }
+ }
+
+ ///<inheritdoc/>
+ public IEnumerator<T> GetEnumerator()
+ {
+ Check();
+ //Enter the semaphore
+ using SemSlimReleaser releader = StorageLock.GetReleaser();
+ foreach (T item in Storage)
+ {
+ yield return item;
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ Check();
+ //Enter the semaphore
+ using SemSlimReleaser releader = StorageLock.GetReleaser();
+ foreach (T item in Storage)
+ {
+ yield return item;
+ }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/Caching/ObjectRentalBase.cs b/lib/Utils/src/Memory/Caching/ObjectRentalBase.cs
new file mode 100644
index 0000000..305d93f
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/ObjectRentalBase.cs
@@ -0,0 +1,155 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ObjectRentalBase.cs
+*
+* ObjectRentalBase.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory.Caching
+{
+ /// <summary>
+ /// Provides concurrent storage for reusable objects to be rented and returned. This class
+ /// and its members is thread-safe
+ /// </summary>
+ public abstract class ObjectRental : VnDisposeable
+ {
+ /// <summary>
+ /// Creates a new <see cref="ObjectRental{T}"/> store
+ /// </summary>
+ /// <param name="quota">The maximum number of elements that will be cached</param>
+ public static ObjectRental<TNew> Create<TNew>(int quota = 0) where TNew : class, new()
+ {
+ static TNew constructor() => new();
+ return new ObjectRental<TNew>(constructor, null, null, quota);
+ }
+ /// <summary>
+ /// Creates a new <see cref="ObjectRental{T}"/> store with generic rental and return callback handlers
+ /// </summary>
+ /// <param name="rentCb">Function responsible for preparing an instance to be rented</param>
+ /// <param name="returnCb">Function responsible for cleaning up an instance before reuse</param>
+ /// <param name="quota">The maximum number of elements that will be cached</param>
+ public static ObjectRental<TNew> Create<TNew>(Action<TNew>? rentCb, Action<TNew>? returnCb, int quota = 0) where TNew : class, new()
+ {
+ static TNew constructor() => new();
+ return new ObjectRental<TNew>(constructor, rentCb, returnCb, quota);
+ }
+ /// <summary>
+ /// Creates a new <see cref="ObjectRental{T}"/> store with a generic constructor function
+ /// </summary>
+ /// <param name="constructor">The function invoked to create a new instance when required</param>
+ /// <param name="quota">The maximum number of elements that will be cached</param>
+ /// <returns></returns>
+ public static ObjectRental<TNew> Create<TNew>(Func<TNew> constructor, int quota = 0) where TNew: class
+ {
+ return new ObjectRental<TNew>(constructor, null, null, quota);
+ }
+ /// <summary>
+ /// Creates a new <see cref="ObjectRental{T}"/> store with generic rental and return callback handlers
+ /// </summary>
+ /// <param name="constructor">The function invoked to create a new instance when required</param>
+ /// <param name="rentCb">Function responsible for preparing an instance to be rented</param>
+ /// <param name="returnCb">Function responsible for cleaning up an instance before reuse</param>
+ /// <param name="quota">The maximum number of elements that will be cached</param>
+ public static ObjectRental<TNew> Create<TNew>(Func<TNew> constructor, Action<TNew>? rentCb, Action<TNew>? returnCb, int quota = 0) where TNew : class
+ {
+ return new ObjectRental<TNew>(constructor, rentCb, returnCb, quota);
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="ThreadLocalObjectStorage{TNew}"/> store with generic rental and return callback handlers
+ /// </summary>
+ /// <typeparam name="TNew"></typeparam>
+ /// <param name="constructor">The function invoked to create a new instance when required</param>
+ /// <param name="rentCb">Function responsible for preparing an instance to be rented</param>
+ /// <param name="returnCb">Function responsible for cleaning up an instance before reuse</param>
+ /// <returns>The initialized store</returns>
+ public static ThreadLocalObjectStorage<TNew> CreateThreadLocal<TNew>(Func<TNew> constructor, Action<TNew>? rentCb, Action<TNew>? returnCb) where TNew : class
+ {
+ return new ThreadLocalObjectStorage<TNew>(constructor, rentCb, returnCb);
+ }
+ /// <summary>
+ /// Creates a new <see cref="ThreadLocalObjectStorage{T}"/> store with generic rental and return callback handlers
+ /// </summary>
+ /// <param name="rentCb">Function responsible for preparing an instance to be rented</param>
+ /// <param name="returnCb">Function responsible for cleaning up an instance before reuse</param>
+ public static ThreadLocalObjectStorage<TNew> CreateThreadLocal<TNew>(Action<TNew>? rentCb, Action<TNew>? returnCb) where TNew : class, new()
+ {
+ static TNew constructor() => new();
+ return new ThreadLocalObjectStorage<TNew>(constructor, rentCb, returnCb);
+ }
+ /// <summary>
+ /// Creates a new <see cref="ThreadLocalObjectStorage{T}"/> store
+ /// </summary>
+ public static ThreadLocalObjectStorage<TNew> CreateThreadLocal<TNew>() where TNew : class, new()
+ {
+ static TNew constructor() => new();
+ return new ThreadLocalObjectStorage<TNew>(constructor, null, null);
+ }
+ /// <summary>
+ /// Creates a new <see cref="ThreadLocalObjectStorage{T}"/> store with a generic constructor function
+ /// </summary>
+ /// <param name="constructor">The function invoked to create a new instance when required</param>
+ /// <returns></returns>
+ public static ThreadLocalObjectStorage<TNew> CreateThreadLocal<TNew>(Func<TNew> constructor) where TNew : class
+ {
+ return new ThreadLocalObjectStorage<TNew>(constructor, null, null);
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="ReusableStore{T}"/> instance with a parameterless constructor
+ /// </summary>
+ /// <typeparam name="T">The <see cref="IReusable"/> type</typeparam>
+ /// <param name="quota">The maximum number of elements that will be cached</param>
+ /// <returns></returns>
+ public static ReusableStore<T> CreateReusable<T>(int quota = 0) where T : class, IReusable, new()
+ {
+ static T constructor() => new();
+ return new(constructor, quota);
+ }
+ /// <summary>
+ /// Creates a new <see cref="ReusableStore{T}"/> instance with the specified constructor
+ /// </summary>
+ /// <typeparam name="T">The <see cref="IReusable"/> type</typeparam>
+ /// <param name="constructor">The constructor function invoked to create new instances of the <see cref="IReusable"/> type</param>
+ /// <param name="quota">The maximum number of elements that will be cached</param>
+ /// <returns></returns>
+ public static ReusableStore<T> CreateReusable<T>(Func<T> constructor, int quota = 0) where T : class, IReusable => new(constructor, quota);
+
+ /// <summary>
+ /// Creates a new <see cref="ThreadLocalReusableStore{T}"/> instance with a parameterless constructor
+ /// </summary>
+ /// <typeparam name="T">The <see cref="IReusable"/> type</typeparam>
+ /// <returns></returns>
+ public static ThreadLocalReusableStore<T> CreateThreadLocalReusable<T>() where T : class, IReusable, new()
+ {
+ static T constructor() => new();
+ return new(constructor);
+ }
+ /// <summary>
+ /// Creates a new <see cref="ThreadLocalReusableStore{T}"/> instance with the specified constructor
+ /// </summary>
+ /// <typeparam name="T">The <see cref="IReusable"/> type</typeparam>
+ /// <param name="constructor">The constructor function invoked to create new instances of the <see cref="IReusable"/> type</param>
+ /// <returns></returns>
+ public static ThreadLocalReusableStore<T> CreateThreadLocalReusable<T>(Func<T> constructor) where T : class, IReusable => new(constructor);
+ }
+}
diff --git a/lib/Utils/src/Memory/Caching/ReusableStore.cs b/lib/Utils/src/Memory/Caching/ReusableStore.cs
new file mode 100644
index 0000000..aacd012
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/ReusableStore.cs
@@ -0,0 +1,61 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ReusableStore.cs
+*
+* ReusableStore.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory.Caching
+{
+
+ /// <summary>
+ /// A reusable object store that extends <see cref="ObjectRental{T}"/>, that allows for objects to be reused heavily
+ /// </summary>
+ /// <typeparam name="T">A reusable object</typeparam>
+ public class ReusableStore<T> : ObjectRental<T> where T : class, IReusable
+ {
+ internal ReusableStore(Func<T> constructor, int quota) :base(constructor, null, null, quota)
+ {}
+ ///<inheritdoc/>
+ public override T Rent()
+ {
+ //Rent the object (or create it)
+ T rental = base.Rent();
+ //Invoke prepare function
+ rental.Prepare();
+ //return object
+ return rental;
+ }
+ ///<inheritdoc/>
+ public override void Return(T item)
+ {
+ /*
+ * Clean up the item by invoking the cleanup function,
+ * and only return the item for reuse if the caller allows
+ */
+ if (item.Release())
+ {
+ base.Return(item);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/Caching/ThreadLocalObjectStorage.cs b/lib/Utils/src/Memory/Caching/ThreadLocalObjectStorage.cs
new file mode 100644
index 0000000..511af24
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/ThreadLocalObjectStorage.cs
@@ -0,0 +1,76 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ThreadLocalObjectStorage.cs
+*
+* ThreadLocalObjectStorage.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+
+namespace VNLib.Utils.Memory.Caching
+{
+ /// <summary>
+ /// Derrives from <see cref="ObjectRental{T}"/> to provide object rental syntax for <see cref="ThreadLocal{T}"/>
+ /// storage
+ /// </summary>
+ /// <typeparam name="T">The data type to store</typeparam>
+ public class ThreadLocalObjectStorage<T> : ObjectRental<T> where T: class
+ {
+ protected ThreadLocal<T> Store { get; }
+
+ internal ThreadLocalObjectStorage(Func<T> constructor, Action<T>? rentCb, Action<T>? returnCb)
+ :base(constructor, rentCb, returnCb, 0)
+ {
+ Store = new(Constructor);
+ }
+
+ /// <summary>
+ /// "Rents" or creates an object for the current thread
+ /// </summary>
+ /// <returns>The new or stored instanced</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public override T Rent()
+ {
+ Check();
+ //Get the tlocal value
+ T value = Store.Value!;
+ //Invoke the rent action if set
+ base.RentAction?.Invoke(value);
+ return value;
+ }
+
+ /// <inheritdoc/>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public override void Return(T item)
+ {
+ Check();
+ //Invoke the rent action
+ base.ReturnAction?.Invoke(item);
+ }
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ Store.Dispose();
+ base.Free();
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/Caching/ThreadLocalReusableStore.cs b/lib/Utils/src/Memory/Caching/ThreadLocalReusableStore.cs
new file mode 100644
index 0000000..83cd4d6
--- /dev/null
+++ b/lib/Utils/src/Memory/Caching/ThreadLocalReusableStore.cs
@@ -0,0 +1,64 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ThreadLocalReusableStore.cs
+*
+* ThreadLocalReusableStore.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory.Caching
+{
+ /// <summary>
+ /// A reusable object store that extends <see cref="ThreadLocalObjectStorage{T}"/>, that allows for objects to be reused heavily
+ /// in a thread-local cache
+ /// </summary>
+ /// <typeparam name="T">A reusable object</typeparam>
+ public class ThreadLocalReusableStore<T> : ThreadLocalObjectStorage<T> where T: class, IReusable
+ {
+ /// <summary>
+ /// Creates a new <see cref="ThreadLocalReusableStore{T}"/> instance
+ /// </summary>
+ internal ThreadLocalReusableStore(Func<T> constructor):base(constructor, null, null)
+ { }
+ ///<inheritdoc/>
+ public override T Rent()
+ {
+ //Rent the object (or create it)
+ T rental = base.Rent();
+ //Invoke prepare function
+ rental.Prepare();
+ //return object
+ return rental;
+ }
+ ///<inheritdoc/>
+ public override void Return(T item)
+ {
+ /*
+ * Clean up the item by invoking the cleanup function,
+ * and only return the item for reuse if the caller allows
+ */
+ if (item.Release())
+ {
+ base.Return(item);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/ForwardOnlyBufferWriter.cs b/lib/Utils/src/Memory/ForwardOnlyBufferWriter.cs
new file mode 100644
index 0000000..0ea507e
--- /dev/null
+++ b/lib/Utils/src/Memory/ForwardOnlyBufferWriter.cs
@@ -0,0 +1,122 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ForwardOnlyBufferWriter.cs
+*
+* ForwardOnlyBufferWriter.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Provides a stack based buffer writer
+ /// </summary>
+ public ref struct ForwardOnlyWriter<T>
+ {
+ /// <summary>
+ /// The buffer for writing output data to
+ /// </summary>
+ public readonly Span<T> Buffer { get; }
+ /// <summary>
+ /// The number of characters written to the buffer
+ /// </summary>
+ public int Written { readonly get; set; }
+ /// <summary>
+ /// The number of characters remaining in the buffer
+ /// </summary>
+ public readonly int RemainingSize => Buffer.Length - Written;
+
+ /// <summary>
+ /// The remaining buffer window
+ /// </summary>
+ public readonly Span<T> Remaining => Buffer[Written..];
+
+ /// <summary>
+ /// Creates a new <see cref="ForwardOnlyWriter{T}"/> assigning the specified buffer
+ /// </summary>
+ /// <param name="buffer">The buffer to write data to</param>
+ public ForwardOnlyWriter(in Span<T> buffer)
+ {
+ Buffer = buffer;
+ Written = 0;
+ }
+
+ /// <summary>
+ /// Returns a compiled string from the characters written to the buffer
+ /// </summary>
+ /// <returns>A string of the characters written to the buffer</returns>
+ public readonly override string ToString() => Buffer[..Written].ToString();
+
+ /// <summary>
+ /// Appends a sequence to the buffer
+ /// </summary>
+ /// <param name="data">The data to append to the buffer</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public void Append(ReadOnlySpan<T> data)
+ {
+ //Make sure the current window is large enough to buffer the new string
+ if (data.Length > RemainingSize)
+ {
+ throw new ArgumentOutOfRangeException(nameof(Remaining) ,"The internal buffer does not have enough buffer space");
+ }
+ Span<T> window = Buffer[Written..];
+ //write data to window
+ data.CopyTo(window);
+ //update char position
+ Written += data.Length;
+ }
+ /// <summary>
+ /// Appends a single item to the buffer
+ /// </summary>
+ /// <param name="c">The item to append to the buffer</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public void Append(T c)
+ {
+ //Make sure the current window is large enough to buffer the new string
+ if (RemainingSize == 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(Remaining), "The internal buffer does not have enough buffer space");
+ }
+ //Write data to buffer and increment the buffer position
+ Buffer[Written++] = c;
+ }
+
+ /// <summary>
+ /// Advances the writer forward the specifed number of elements
+ /// </summary>
+ /// <param name="count">The number of elements to advance the writer by</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public void Advance(int count)
+ {
+ if (count > RemainingSize)
+ {
+ throw new ArgumentOutOfRangeException(nameof(Remaining), "The internal buffer does not have enough buffer space");
+ }
+ Written += count;
+ }
+
+ /// <summary>
+ /// Resets the writer by setting the <see cref="Written"/>
+ /// property to 0.
+ /// </summary>
+ public void Reset() => Written = 0;
+ }
+}
diff --git a/lib/Utils/src/Memory/ForwardOnlyMemoryReader.cs b/lib/Utils/src/Memory/ForwardOnlyMemoryReader.cs
new file mode 100644
index 0000000..c850b14
--- /dev/null
+++ b/lib/Utils/src/Memory/ForwardOnlyMemoryReader.cs
@@ -0,0 +1,74 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ForwardOnlyMemoryReader.cs
+*
+* ForwardOnlyMemoryReader.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// A mutable structure used to implement a simple foward only
+ /// reader for a memory segment
+ /// </summary>
+ /// <typeparam name="T">The element type</typeparam>
+ public struct ForwardOnlyMemoryReader<T>
+ {
+ private readonly ReadOnlyMemory<T> _segment;
+ private readonly int _size;
+
+ private int _position;
+
+ /// <summary>
+ /// Initializes a new <see cref="FordwardOnlyMemoryReader{T}"/>
+ /// of the specified type using the specified internal buffer
+ /// </summary>
+ /// <param name="buffer">The buffer to read from</param>
+ public ForwardOnlyMemoryReader(in ReadOnlyMemory<T> buffer)
+ {
+ _segment = buffer;
+ _size = buffer.Length;
+ _position = 0;
+ }
+
+ /// <summary>
+ /// The remaining data window
+ /// </summary>
+ public readonly ReadOnlyMemory<T> Window => _segment[_position..];
+ /// <summary>
+ /// The number of elements remaining in the window
+ /// </summary>
+ public readonly int WindowSize => _size - _position;
+
+
+ /// <summary>
+ /// Advances the window position the specified number of elements
+ /// </summary>
+ /// <param name="count">The number of elements to advance the widnow position</param>
+ public void Advance(int count) => _position += count;
+
+ /// <summary>
+ /// Resets the sliding window to the begining of the buffer
+ /// </summary>
+ public void Reset() => _position = 0;
+ }
+}
diff --git a/lib/Utils/src/Memory/ForwardOnlyMemoryWriter.cs b/lib/Utils/src/Memory/ForwardOnlyMemoryWriter.cs
new file mode 100644
index 0000000..4f5286d
--- /dev/null
+++ b/lib/Utils/src/Memory/ForwardOnlyMemoryWriter.cs
@@ -0,0 +1,122 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ForwardOnlyMemoryWriter.cs
+*
+* ForwardOnlyMemoryWriter.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Provides a mutable sliding buffer writer
+ /// </summary>
+ public struct ForwardOnlyMemoryWriter<T>
+ {
+ /// <summary>
+ /// The buffer for writing output data to
+ /// </summary>
+ public readonly Memory<T> Buffer { get; }
+ /// <summary>
+ /// The number of characters written to the buffer
+ /// </summary>
+ public int Written { readonly get; set; }
+ /// <summary>
+ /// The number of characters remaining in the buffer
+ /// </summary>
+ public readonly int RemainingSize => Buffer.Length - Written;
+
+ /// <summary>
+ /// The remaining buffer window
+ /// </summary>
+ public readonly Memory<T> Remaining => Buffer[Written..];
+
+ /// <summary>
+ /// Creates a new <see cref="ForwardOnlyWriter{T}"/> assigning the specified buffer
+ /// </summary>
+ /// <param name="buffer">The buffer to write data to</param>
+ public ForwardOnlyMemoryWriter(in Memory<T> buffer)
+ {
+ Buffer = buffer;
+ Written = 0;
+ }
+
+ /// <summary>
+ /// Returns a compiled string from the characters written to the buffer
+ /// </summary>
+ /// <returns>A string of the characters written to the buffer</returns>
+ public readonly override string ToString() => Buffer[..Written].ToString();
+
+ /// <summary>
+ /// Appends a sequence to the buffer
+ /// </summary>
+ /// <param name="data">The data to append to the buffer</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public void Append(ReadOnlyMemory<T> data)
+ {
+ //Make sure the current window is large enough to buffer the new string
+ if (data.Length > RemainingSize)
+ {
+ throw new ArgumentOutOfRangeException(nameof(Remaining), "The internal buffer does not have enough buffer space");
+ }
+ Memory<T> window = Buffer[Written..];
+ //write data to window
+ data.CopyTo(window);
+ //update char position
+ Written += data.Length;
+ }
+ /// <summary>
+ /// Appends a single item to the buffer
+ /// </summary>
+ /// <param name="c">The item to append to the buffer</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public void Append(T c)
+ {
+ //Make sure the current window is large enough to buffer the new string
+ if (RemainingSize == 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(Remaining), "The internal buffer does not have enough buffer space");
+ }
+ //Write data to buffer and increment the buffer position
+ Buffer.Span[Written++] = c;
+ }
+
+ /// <summary>
+ /// Advances the writer forward the specifed number of elements
+ /// </summary>
+ /// <param name="count">The number of elements to advance the writer by</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public void Advance(int count)
+ {
+ if (count > RemainingSize)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count), count, "Cannot advance past the end of the buffer");
+ }
+ Written += count;
+ }
+
+ /// <summary>
+ /// Resets the writer by setting the <see cref="Written"/>
+ /// property to 0.
+ /// </summary>
+ public void Reset() => Written = 0;
+ }
+}
diff --git a/lib/Utils/src/Memory/ForwardOnlyReader.cs b/lib/Utils/src/Memory/ForwardOnlyReader.cs
new file mode 100644
index 0000000..aa268c4
--- /dev/null
+++ b/lib/Utils/src/Memory/ForwardOnlyReader.cs
@@ -0,0 +1,74 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ForwardOnlyReader.cs
+*
+* ForwardOnlyReader.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// A mutable structure used to implement a simple foward only
+ /// reader for a memory segment
+ /// </summary>
+ /// <typeparam name="T">The element type</typeparam>
+ public ref struct ForwardOnlyReader<T>
+ {
+ private readonly ReadOnlySpan<T> _segment;
+ private readonly int _size;
+
+ private int _position;
+
+ /// <summary>
+ /// Initializes a new <see cref="FordwardOnlyReader{T}"/>
+ /// of the specified type using the specified internal buffer
+ /// </summary>
+ /// <param name="buffer">The buffer to read from</param>
+ public ForwardOnlyReader(in ReadOnlySpan<T> buffer)
+ {
+ _segment = buffer;
+ _size = buffer.Length;
+ _position = 0;
+ }
+
+ /// <summary>
+ /// The remaining data window
+ /// </summary>
+ public readonly ReadOnlySpan <T> Window => _segment[_position..];
+
+ /// <summary>
+ /// The number of elements remaining in the window
+ /// </summary>
+ public readonly int WindowSize => _size - _position;
+
+ /// <summary>
+ /// Advances the window position the specified number of elements
+ /// </summary>
+ /// <param name="count">The number of elements to advance the widnow position</param>
+ public void Advance(int count) => _position += count;
+
+ /// <summary>
+ /// Resets the sliding window to the begining of the buffer
+ /// </summary>
+ public void Reset() => _position = 0;
+ }
+}
diff --git a/lib/Utils/src/Memory/IMemoryHandle.cs b/lib/Utils/src/Memory/IMemoryHandle.cs
new file mode 100644
index 0000000..75d1cce
--- /dev/null
+++ b/lib/Utils/src/Memory/IMemoryHandle.cs
@@ -0,0 +1,53 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IMemoryHandle.cs
+*
+* IMemoryHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Represents a handle for safe access to memory managed/unamanged memory
+ /// </summary>
+ /// <typeparam name="T">The type this handle represents</typeparam>
+ public interface IMemoryHandle<T> : IDisposable, IPinnable
+ {
+ /// <summary>
+ /// The size of the block as an integer
+ /// </summary>
+ /// <exception cref="OverflowException"></exception>
+ int IntLength { get; }
+
+ /// <summary>
+ /// The number of elements in the block
+ /// </summary>
+ ulong Length { get; }
+
+ /// <summary>
+ /// Gets the internal block as a span
+ /// </summary>
+ Span<T> Span { get; }
+ }
+
+}
diff --git a/lib/Utils/src/Memory/IStringSerializeable.cs b/lib/Utils/src/Memory/IStringSerializeable.cs
new file mode 100644
index 0000000..12cfe52
--- /dev/null
+++ b/lib/Utils/src/Memory/IStringSerializeable.cs
@@ -0,0 +1,55 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IStringSerializeable.cs
+*
+* IStringSerializeable.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// A interface that provides indempodent abstractions for compiling an instance
+ /// to its representitive string.
+ /// </summary>
+ public interface IStringSerializeable
+ {
+ /// <summary>
+ /// Compiles the current instance into its safe string representation
+ /// </summary>
+ /// <returns>A string of the desired representation of the current instance</returns>
+ string Compile();
+ /// <summary>
+ /// Compiles the current instance into its safe string representation, and writes its
+ /// contents to the specified buffer writer
+ /// </summary>
+ /// <param name="writer">The ouput writer to write the serialized representation to</param>
+ /// <exception cref="OutOfMemoryException"></exception>
+ void Compile(ref ForwardOnlyWriter<char> writer);
+ /// <summary>
+ /// Compiles the current instance into its safe string representation, and writes its
+ /// contents to the specified buffer writer
+ /// </summary>
+ /// <param name="buffer">The buffer to write the serialized representation to</param>
+ /// <returns>The number of characters written to the buffer</returns>
+ ERRNO Compile(in Span<char> buffer);
+ }
+}
diff --git a/lib/Utils/src/Memory/IUnmangedHeap.cs b/lib/Utils/src/Memory/IUnmangedHeap.cs
new file mode 100644
index 0000000..5d8f4bf
--- /dev/null
+++ b/lib/Utils/src/Memory/IUnmangedHeap.cs
@@ -0,0 +1,59 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IUnmangedHeap.cs
+*
+* IUnmangedHeap.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Abstraction for handling (allocating, resizing, and freeing) blocks of unmanaged memory from an unmanged heap
+ /// </summary>
+ public interface IUnmangedHeap : IDisposable
+ {
+ /// <summary>
+ /// Allocates a block of memory from the heap and returns a pointer to the new memory block
+ /// </summary>
+ /// <param name="size">The size (in bytes) of the element</param>
+ /// <param name="elements">The number of elements to allocate</param>
+ /// <param name="zero">An optional parameter to zero the block of memory</param>
+ /// <returns></returns>
+ IntPtr Alloc(UInt64 elements, UInt64 size, bool zero);
+
+ /// <summary>
+ /// Resizes the allocated block of memory to the new size
+ /// </summary>
+ /// <param name="block">The block to resize</param>
+ /// <param name="elements">The new number of elements</param>
+ /// <param name="size">The size (in bytes) of the type</param>
+ /// <param name="zero">An optional parameter to zero the block of memory</param>
+ void Resize(ref IntPtr block, UInt64 elements, UInt64 size, bool zero);
+
+ /// <summary>
+ /// Free's a previously allocated block of memory
+ /// </summary>
+ /// <param name="block">The memory to be freed</param>
+ /// <returns>A value indicating if the free operation succeeded</returns>
+ bool Free(ref IntPtr block);
+ }
+}
diff --git a/lib/Utils/src/Memory/Memory.cs b/lib/Utils/src/Memory/Memory.cs
new file mode 100644
index 0000000..e04c386
--- /dev/null
+++ b/lib/Utils/src/Memory/Memory.cs
@@ -0,0 +1,456 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: Memory.cs
+*
+* Memory.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Buffers;
+using System.Security;
+using System.Threading;
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Provides optimized cross-platform maanged/umanaged safe/unsafe memory operations
+ /// </summary>
+ [SecurityCritical]
+ [ComVisible(false)]
+ public unsafe static class Memory
+ {
+ public const string SHARED_HEAP_TYPE_ENV= "VNLIB_SHARED_HEAP_TYPE";
+ public const string SHARED_HEAP_INTIAL_SIZE_ENV = "VNLIB_SHARED_HEAP_SIZE";
+
+ /// <summary>
+ /// Initial shared heap size (bytes)
+ /// </summary>
+ public const ulong SHARED_HEAP_INIT_SIZE = 20971520;
+
+ public const int MAX_BUF_SIZE = 2097152;
+ public const int MIN_BUF_SIZE = 16000;
+
+ /// <summary>
+ /// The maximum buffer size requested by <see cref="UnsafeAlloc{T}(int, bool)"/>
+ /// that will use the array pool before falling back to the <see cref="Shared"/>.
+ /// heap.
+ /// </summary>
+ public const int MAX_UNSAFE_POOL_SIZE = 500 * 1024;
+
+ /// <summary>
+ /// Provides a shared heap instance for the process to allocate memory from.
+ /// </summary>
+ /// <remarks>
+ /// The backing heap
+ /// is determined by the OS type and process environment varibles.
+ /// </remarks>
+ public static IUnmangedHeap Shared => _sharedHeap.Value;
+
+ private static readonly Lazy<IUnmangedHeap> _sharedHeap;
+
+ static Memory()
+ {
+ _sharedHeap = new Lazy<IUnmangedHeap>(() => InitHeapInternal(true), LazyThreadSafetyMode.PublicationOnly);
+ //Cleanup the heap on process exit
+ AppDomain.CurrentDomain.DomainUnload += DomainUnloaded;
+ }
+
+ private static void DomainUnloaded(object? sender, EventArgs e)
+ {
+ //Dispose the heap if allocated
+ if (_sharedHeap.IsValueCreated)
+ {
+ _sharedHeap.Value.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new <see cref="IUnmangedHeap"/> determined by compilation/runtime flags
+ /// and operating system type for the current proccess.
+ /// </summary>
+ /// <returns>An <see cref="IUnmangedHeap"/> for the current process</returns>
+ /// <exception cref="SystemException"></exception>
+ /// <exception cref="DllNotFoundException"></exception>
+ public static IUnmangedHeap InitializeNewHeapForProcess() => InitHeapInternal(false);
+
+ private static IUnmangedHeap InitHeapInternal(bool isShared)
+ {
+ bool IsWindows = OperatingSystem.IsWindows();
+ //Get environment varable
+ string heapType = Environment.GetEnvironmentVariable(SHARED_HEAP_TYPE_ENV);
+ //Get inital size
+ string sharedSize = Environment.GetEnvironmentVariable(SHARED_HEAP_INTIAL_SIZE_ENV);
+ //Try to parse the shared size from the env
+ if (!ulong.TryParse(sharedSize, out ulong defaultSize))
+ {
+ defaultSize = SHARED_HEAP_INIT_SIZE;
+ }
+ //Gen the private heap from its type or default
+ switch (heapType)
+ {
+ case "win32":
+ if (!IsWindows)
+ {
+ throw new PlatformNotSupportedException("Win32 private heaps are not supported on non-windows platforms");
+ }
+ return PrivateHeap.Create(defaultSize);
+ case "rpmalloc":
+ //If the shared heap is being allocated, then return a lock free global heap
+ return isShared ? RpMallocPrivateHeap.GlobalHeap : new RpMallocPrivateHeap(false);
+ default:
+ return IsWindows ? PrivateHeap.Create(defaultSize) : new ProcessHeap();
+ }
+ }
+
+ /// <summary>
+ /// Gets a value that indicates if the Rpmalloc native library is loaded
+ /// </summary>
+ public static bool IsRpMallocLoaded { get; } = Environment.GetEnvironmentVariable(SHARED_HEAP_TYPE_ENV) == "rpmalloc";
+
+ #region Zero
+ /// <summary>
+ /// Zeros a block of memory of umanged type. If Windows is detected at runtime, calls RtlSecureZeroMemory Win32 function
+ /// </summary>
+ /// <typeparam name="T">Unmanged datatype</typeparam>
+ /// <param name="block">Block of memory to be cleared</param>
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ public static void UnsafeZeroMemory<T>(ReadOnlySpan<T> block) where T : unmanaged
+ {
+ if (!block.IsEmpty)
+ {
+ checked
+ {
+ fixed (void* ptr = &MemoryMarshal.GetReference(block))
+ {
+ //Calls memset
+ Unsafe.InitBlock(ptr, 0, (uint)(block.Length * sizeof(T)));
+ }
+ }
+ }
+ }
+ /// <summary>
+ /// Zeros a block of memory of umanged type. If Windows is detected at runtime, calls RtlSecureZeroMemory Win32 function
+ /// </summary>
+ /// <typeparam name="T">Unmanged datatype</typeparam>
+ /// <param name="block">Block of memory to be cleared</param>
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ public static void UnsafeZeroMemory<T>(ReadOnlyMemory<T> block) where T : unmanaged
+ {
+ if (!block.IsEmpty)
+ {
+ checked
+ {
+ //Pin memory and get pointer
+ using MemoryHandle handle = block.Pin();
+ //Calls memset
+ Unsafe.InitBlock(handle.Pointer, 0, (uint)(block.Length * sizeof(T)));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Initializes a block of memory with zeros
+ /// </summary>
+ /// <typeparam name="T">The unmanaged</typeparam>
+ /// <param name="block">The block of memory to initialize</param>
+ public static void InitializeBlock<T>(Span<T> block) where T : unmanaged => UnsafeZeroMemory<T>(block);
+ /// <summary>
+ /// Initializes a block of memory with zeros
+ /// </summary>
+ /// <typeparam name="T">The unmanaged</typeparam>
+ /// <param name="block">The block of memory to initialize</param>
+ public static void InitializeBlock<T>(Memory<T> block) where T : unmanaged => UnsafeZeroMemory<T>(block);
+
+ /// <summary>
+ /// Zeroes a block of memory pointing to the structure
+ /// </summary>
+ /// <typeparam name="T">The structure type</typeparam>
+ /// <param name="block">The pointer to the allocated structure</param>
+ public static void ZeroStruct<T>(IntPtr block)
+ {
+ //get thes size of the structure
+ int size = Unsafe.SizeOf<T>();
+ //Zero block
+ Unsafe.InitBlock(block.ToPointer(), 0, (uint)size);
+ }
+ /// <summary>
+ /// Zeroes a block of memory pointing to the structure
+ /// </summary>
+ /// <typeparam name="T">The structure type</typeparam>
+ /// <param name="structPtr">The pointer to the allocated structure</param>
+ public static void ZeroStruct<T>(void* structPtr) where T: unmanaged
+ {
+ //get thes size of the structure
+ int size = Unsafe.SizeOf<T>();
+ //Zero block
+ Unsafe.InitBlock(structPtr, 0, (uint)size);
+ }
+ /// <summary>
+ /// Zeroes a block of memory pointing to the structure
+ /// </summary>
+ /// <typeparam name="T">The structure type</typeparam>
+ /// <param name="structPtr">The pointer to the allocated structure</param>
+ public static void ZeroStruct<T>(T* structPtr) where T : unmanaged
+ {
+ //get thes size of the structure
+ int size = Unsafe.SizeOf<T>();
+ //Zero block
+ Unsafe.InitBlock(structPtr, 0, (uint)size);
+ }
+
+ #endregion
+
+ #region Copy
+ /// <summary>
+ /// Copies data from source memory to destination memory of an umanged data type
+ /// </summary>
+ /// <typeparam name="T">Unmanged type</typeparam>
+ /// <param name="source">Source data <see cref="ReadOnlySpan{T}"/></param>
+ /// <param name="dest">Destination <see cref="MemoryHandle{T}"/></param>
+ /// <param name="destOffset">Dest offset</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static void Copy<T>(ReadOnlySpan<T> source, MemoryHandle<T> dest, Int64 destOffset) where T : unmanaged
+ {
+ if (source.IsEmpty)
+ {
+ return;
+ }
+ if (dest.Length < (ulong)(destOffset + source.Length))
+ {
+ throw new ArgumentException("Source data is larger than the dest data block", nameof(source));
+ }
+ //Get long offset from the destination handle
+ T* offset = dest.GetOffset(destOffset);
+ fixed(void* src = &MemoryMarshal.GetReference(source))
+ {
+ int byteCount = checked(source.Length * sizeof(T));
+ Unsafe.CopyBlock(offset, src, (uint)byteCount);
+ }
+ }
+ /// <summary>
+ /// Copies data from source memory to destination memory of an umanged data type
+ /// </summary>
+ /// <typeparam name="T">Unmanged type</typeparam>
+ /// <param name="source">Source data <see cref="ReadOnlyMemory{T}"/></param>
+ /// <param name="dest">Destination <see cref="MemoryHandle{T}"/></param>
+ /// <param name="destOffset">Dest offset</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static void Copy<T>(ReadOnlyMemory<T> source, MemoryHandle<T> dest, Int64 destOffset) where T : unmanaged
+ {
+ if (source.IsEmpty)
+ {
+ return;
+ }
+ if (dest.Length < (ulong)(destOffset + source.Length))
+ {
+ throw new ArgumentException("Dest constraints are larger than the dest data block", nameof(source));
+ }
+ //Get long offset from the destination handle
+ T* offset = dest.GetOffset(destOffset);
+ //Pin the source memory
+ using MemoryHandle srcHandle = source.Pin();
+ int byteCount = checked(source.Length * sizeof(T));
+ //Copy block using unsafe class
+ Unsafe.CopyBlock(offset, srcHandle.Pointer, (uint)byteCount);
+ }
+ /// <summary>
+ /// Copies data from source memory to destination memory of an umanged data type
+ /// </summary>
+ /// <typeparam name="T">Unmanged type</typeparam>
+ /// <param name="source">Source data <see cref="MemoryHandle{T}"/></param>
+ /// <param name="sourceOffset">Number of elements to offset source data</param>
+ /// <param name="dest">Destination <see cref="Span{T}"/></param>
+ /// <param name="destOffset">Dest offset</param>
+ /// <param name="count">Number of elements to copy</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static void Copy<T>(MemoryHandle<T> source, Int64 sourceOffset, Span<T> dest, int destOffset, int count) where T : unmanaged
+ {
+ if (count <= 0)
+ {
+ return;
+ }
+ if (source.Length < (ulong)(sourceOffset + count))
+ {
+ throw new ArgumentException("Source constraints are larger than the source data block", nameof(count));
+ }
+ if (dest.Length < destOffset + count)
+ {
+ throw new ArgumentOutOfRangeException(nameof(destOffset), "Destination offset range cannot exceed the size of the destination buffer");
+ }
+ //Get offset to allow large blocks of memory
+ T* src = source.GetOffset(sourceOffset);
+ fixed(T* dst = &MemoryMarshal.GetReference(dest))
+ {
+ //Cacl offset
+ T* dstoffset = dst + destOffset;
+ int byteCount = checked(count * sizeof(T));
+ //Aligned copy
+ Unsafe.CopyBlock(dstoffset, src, (uint)byteCount);
+ }
+ }
+ /// <summary>
+ /// Copies data from source memory to destination memory of an umanged data type
+ /// </summary>
+ /// <typeparam name="T">Unmanged type</typeparam>
+ /// <param name="source">Source data <see cref="MemoryHandle{T}"/></param>
+ /// <param name="sourceOffset">Number of elements to offset source data</param>
+ /// <param name="dest">Destination <see cref="Memory{T}"/></param>
+ /// <param name="destOffset">Dest offset</param>
+ /// <param name="count">Number of elements to copy</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static void Copy<T>(MemoryHandle<T> source, Int64 sourceOffset, Memory<T> dest, int destOffset, int count) where T : unmanaged
+ {
+ if (count == 0)
+ {
+ return;
+ }
+ if (source.Length < (ulong)(sourceOffset + count))
+ {
+ throw new ArgumentException("Source constraints are larger than the source data block", nameof(count));
+ }
+ if(dest.Length < destOffset + count)
+ {
+ throw new ArgumentOutOfRangeException(nameof(destOffset), "Destination offset range cannot exceed the size of the destination buffer");
+ }
+ //Get offset to allow large blocks of memory
+ T* src = source.GetOffset(sourceOffset);
+ //Pin the memory handle
+ using MemoryHandle handle = dest.Pin();
+ //Byte count
+ int byteCount = checked(count * sizeof(T));
+ //Dest offset
+ T* dst = ((T*)handle.Pointer) + destOffset;
+ //Aligned copy
+ Unsafe.CopyBlock(dst, src, (uint)byteCount);
+ }
+ #endregion
+
+ #region Streams
+ /// <summary>
+ /// Copies data from one stream to another in specified blocks
+ /// </summary>
+ /// <param name="source">Source memory</param>
+ /// <param name="srcOffset">Source offset</param>
+ /// <param name="dest">Destination memory</param>
+ /// <param name="destOffst">Destination offset</param>
+ /// <param name="count">Number of elements to copy</param>
+ public static void Copy(Stream source, Int64 srcOffset, Stream dest, Int64 destOffst, Int64 count)
+ {
+ if (count == 0)
+ {
+ return;
+ }
+ if (count < 0)
+ {
+ throw new ArgumentException("Count must be a positive integer", nameof(count));
+ }
+ //Seek streams
+ _ = source.Seek(srcOffset, SeekOrigin.Begin);
+ _ = dest.Seek(destOffst, SeekOrigin.Begin);
+ //Create new buffer
+ using IMemoryHandle<byte> buffer = Shared.Alloc<byte>(count);
+ Span<byte> buf = buffer.Span;
+ int total = 0;
+ do
+ {
+ //read from source
+ int read = source.Read(buf);
+ //guard
+ if (read == 0)
+ {
+ break;
+ }
+ //write read slice to dest
+ dest.Write(buf[..read]);
+ //update total read
+ total += read;
+ } while (total < count);
+ }
+ #endregion
+
+ #region alloc
+
+ /// <summary>
+ /// Allocates a block of unmanaged, or pooled manaaged memory depending on
+ /// compilation flags and runtime unamanged allocators.
+ /// </summary>
+ /// <typeparam name="T">The unamanged type to allocate</typeparam>
+ /// <param name="elements">The number of elements of the type within the block</param>
+ /// <param name="zero">Flag to zero elements during allocation before the method returns</param>
+ /// <returns>A handle to the block of memory</returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ public static UnsafeMemoryHandle<T> UnsafeAlloc<T>(int elements, bool zero = false) where T : unmanaged
+ {
+ if (elements < 0)
+ {
+ throw new ArgumentException("Number of elements must be a positive integer", nameof(elements));
+ }
+ if(elements > MAX_UNSAFE_POOL_SIZE || IsRpMallocLoaded)
+ {
+ // Alloc from heap
+ IntPtr block = Shared.Alloc((uint)elements, (uint)sizeof(T), zero);
+ //Init new handle
+ return new(Shared, block, elements);
+ }
+ else
+ {
+ return new(ArrayPool<T>.Shared, elements, zero);
+ }
+ }
+
+ /// <summary>
+ /// Allocates a block of unmanaged, or pooled manaaged memory depending on
+ /// compilation flags and runtime unamanged allocators.
+ /// </summary>
+ /// <typeparam name="T">The unamanged type to allocate</typeparam>
+ /// <param name="elements">The number of elements of the type within the block</param>
+ /// <param name="zero">Flag to zero elements during allocation before the method returns</param>
+ /// <returns>A handle to the block of memory</returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ public static IMemoryHandle<T> SafeAlloc<T>(int elements, bool zero = false) where T: unmanaged
+ {
+ if (elements < 0)
+ {
+ throw new ArgumentException("Number of elements must be a positive integer", nameof(elements));
+ }
+
+ //If the element count is larger than max pool size, alloc from shared heap
+ if (elements > MAX_UNSAFE_POOL_SIZE)
+ {
+ //Alloc from shared heap
+ return Shared.Alloc<T>(elements, zero);
+ }
+ else
+ {
+ //Get temp buffer from shared buffer pool
+ return new VnTempBuffer<T>(elements, zero);
+ }
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/MemoryHandle.cs b/lib/Utils/src/Memory/MemoryHandle.cs
new file mode 100644
index 0000000..a09edea
--- /dev/null
+++ b/lib/Utils/src/Memory/MemoryHandle.cs
@@ -0,0 +1,237 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: MemoryHandle.cs
+*
+* MemoryHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+
+using Microsoft.Win32.SafeHandles;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Provides a wrapper for using umanged memory handles from an assigned <see cref="PrivateHeap"/> for <see cref="UnmanagedType"/> types
+ /// </summary>
+ /// <remarks>
+ /// Handles are configured to address blocks larger than 2GB,
+ /// so some properties may raise exceptions if large blocks are used.
+ /// </remarks>
+ public sealed class MemoryHandle<T> : SafeHandleZeroOrMinusOneIsInvalid, IMemoryHandle<T>, IEquatable<MemoryHandle<T>> where T : unmanaged
+ {
+ /// <summary>
+ /// New <typeparamref name="T"/>* pointing to the base of the allocated block
+ /// </summary>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public unsafe T* Base
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => GetOffset(0);
+ }
+ /// <summary>
+ /// New <see cref="IntPtr"/> pointing to the base of the allocated block
+ /// </summary>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public unsafe IntPtr BasePtr
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => (IntPtr)GetOffset(0);
+ }
+ /// <summary>
+ /// Gets a span over the entire allocated block
+ /// </summary>
+ /// <returns>A <see cref="Span{T}"/> over the internal data</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="OverflowException"></exception>
+ public unsafe Span<T> Span
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ this.ThrowIfClosed();
+ return _length == 0 ? Span<T>.Empty : new Span<T>(Base, IntLength);
+ }
+ }
+
+ private readonly bool ZeroMemory;
+ private readonly IUnmangedHeap Heap;
+ private ulong _length;
+
+ /// <summary>
+ /// Number of elements allocated to the current instance
+ /// </summary>
+ public ulong Length
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _length;
+ }
+ /// <summary>
+ /// Number of elements in the memory block casted to an integer
+ /// </summary>
+ /// <exception cref="OverflowException"></exception>
+ public int IntLength
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => checked((int)_length);
+ }
+
+ /// <summary>
+ /// Number of bytes allocated to the current instance
+ /// </summary>
+ /// <exception cref="OverflowException"></exception>
+ public unsafe ulong ByteLength
+ {
+ //Check for overflows when converting to bytes (should run out of memory before this is an issue, but just incase)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => checked(_length * (UInt64)sizeof(T));
+ }
+
+ /// <summary>
+ /// Creates a new memory handle, for which is holds ownership, and allocates the number of elements specified on the heap.
+ /// </summary>
+ /// <param name="heap">The heap to allocate/deallocate memory from</param>
+ /// <param name="elements">Number of elements to allocate</param>
+ /// <param name="zero">Zero all memory during allocations from heap</param>
+ /// <param name="initial">The initial block of allocated memory to wrap</param>
+ internal MemoryHandle(IUnmangedHeap heap, IntPtr initial, ulong elements, bool zero) : base(true)
+ {
+ //Set element size (always allocate at least 1 object)
+ _length = elements;
+ ZeroMemory = zero;
+ //assign heap ref
+ Heap = heap;
+ handle = initial;
+ }
+
+ /// <summary>
+ /// Resizes the current handle on the heap
+ /// </summary>
+ /// <param name="elements">Positive number of elemnts the current handle should referrence</param>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public unsafe void Resize(ulong elements)
+ {
+ this.ThrowIfClosed();
+ //Update size (should never be less than inital size)
+ _length = elements;
+ //Re-alloc (Zero if required)
+ try
+ {
+ Heap.Resize(ref handle, Length, (ulong)sizeof(T), ZeroMemory);
+ }
+ //Catch the disposed exception so we can invalidate the current ptr
+ catch (ObjectDisposedException)
+ {
+ base.handle = IntPtr.Zero;
+ //Set as invalid so release does not get called
+ base.SetHandleAsInvalid();
+ //Propagate the exception
+ throw;
+ }
+ }
+ /// <summary>
+ /// Gets an offset pointer from the base postion to the number of bytes specified. Performs bounds checks
+ /// </summary>
+ /// <param name="elements">Number of elements of type to offset</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ /// <returns><typeparamref name="T"/> pointer to the memory offset specified</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public unsafe T* GetOffset(ulong elements)
+ {
+ if (elements >= _length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(elements), "Element offset cannot be larger than allocated size");
+ }
+ this.ThrowIfClosed();
+ //Get ptr and offset it
+ T* bs = ((T*)handle) + elements;
+ return bs;
+ }
+
+ ///<inheritdoc/>
+ ///<exception cref="ObjectDisposedException"></exception>
+ ///<exception cref="ArgumentOutOfRangeException"></exception>
+ public unsafe MemoryHandle Pin(int elementIndex)
+ {
+ //Get ptr and guard checks before adding the referrence
+ T* ptr = GetOffset((ulong)elementIndex);
+
+ bool addRef = false;
+ //use the pinned field as success val
+ DangerousAddRef(ref addRef);
+ //Create a new system.buffers memory handle from the offset ptr address
+ return !addRef
+ ? throw new ObjectDisposedException("Failed to increase referrence count on the memory handle because it was released")
+ : new MemoryHandle(ptr, pinnable: this);
+ }
+
+ ///<inheritdoc/>
+ ///<exception cref="ObjectDisposedException"></exception>
+ public void Unpin()
+ {
+ //Dec count on release
+ DangerousRelease();
+ }
+
+
+ ///<inheritdoc/>
+ protected override bool ReleaseHandle()
+ {
+ //Return result of free
+ return Heap.Free(ref handle);
+ }
+
+
+
+ /// <summary>
+ /// Determines if the memory blocks are equal by comparing their base addresses.
+ /// </summary>
+ /// <param name="other"><see cref="MemoryHandle{T}"/> to compare</param>
+ /// <returns>true if the block of memory is the same, false if the handle's size does not
+ /// match or the base addresses do not match even if they point to an overlapping address space</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public bool Equals(MemoryHandle<T> other)
+ {
+ this.ThrowIfClosed();
+ other.ThrowIfClosed();
+ return _length == other._length && handle == other.handle;
+ }
+ ///<inheritdoc/>
+ public override bool Equals(object obj) => obj is MemoryHandle<T> oHandle && Equals(oHandle);
+ ///<inheritdoc/>
+ public override int GetHashCode() => base.GetHashCode();
+
+
+ ///<inheritdoc/>
+ public static implicit operator Span<T>(MemoryHandle<T> handle)
+ {
+ //If the handle is invalid or closed return an empty span
+ return handle.IsClosed || handle.IsInvalid || handle._length == 0 ? Span<T>.Empty : handle.Span;
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/PrivateBuffersMemoryPool.cs b/lib/Utils/src/Memory/PrivateBuffersMemoryPool.cs
new file mode 100644
index 0000000..1e85207
--- /dev/null
+++ b/lib/Utils/src/Memory/PrivateBuffersMemoryPool.cs
@@ -0,0 +1,67 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: PrivateBuffersMemoryPool.cs
+*
+* PrivateBuffersMemoryPool.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Provides a <see cref="MemoryPool{T}"/> wrapper for using unmanged <see cref="PrivateHeap"/>s
+ /// </summary>
+ /// <typeparam name="T">Unamanged memory type to provide data memory instances from</typeparam>
+ public sealed class PrivateBuffersMemoryPool<T> : MemoryPool<T> where T : unmanaged
+ {
+ private readonly IUnmangedHeap Heap;
+
+ internal PrivateBuffersMemoryPool(IUnmangedHeap heap):base()
+ {
+ this.Heap = heap;
+ }
+ ///<inheritdoc/>
+ public override int MaxBufferSize => int.MaxValue;
+ ///<inheritdoc/>
+ ///<exception cref="OutOfMemoryException"></exception>
+ ///<exception cref="ObjectDisposedException"></exception>
+ ///<exception cref="ArgumentOutOfRangeException"></exception>
+ public override IMemoryOwner<T> Rent(int minBufferSize = 0) => new SysBufferMemoryManager<T>(Heap, (uint)minBufferSize, false);
+
+ /// <summary>
+ /// Allocates a new <see cref="MemoryManager{T}"/> of a different data type from the pool
+ /// </summary>
+ /// <typeparam name="TDifType">The unmanaged data type to allocate for</typeparam>
+ /// <param name="minBufferSize">Minumum size of the buffer</param>
+ /// <returns>The memory owner of a different data type</returns>
+ public IMemoryOwner<TDifType> Rent<TDifType>(int minBufferSize = 0) where TDifType : unmanaged
+ {
+ return new SysBufferMemoryManager<TDifType>(Heap, (uint)minBufferSize, false);
+ }
+ ///<inheritdoc/>
+ protected override void Dispose(bool disposing)
+ {
+ //Dispose the heap
+ Heap.Dispose();
+ }
+ }
+}
diff --git a/lib/Utils/src/Memory/PrivateHeap.cs b/lib/Utils/src/Memory/PrivateHeap.cs
new file mode 100644
index 0000000..5d97506
--- /dev/null
+++ b/lib/Utils/src/Memory/PrivateHeap.cs
@@ -0,0 +1,184 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: PrivateHeap.cs
+*
+* PrivateHeap.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Diagnostics;
+using System.Runtime.Versioning;
+using System.Runtime.InteropServices;
+
+using DWORD = System.Int64;
+using SIZE_T = System.UInt64;
+using LPVOID = System.IntPtr;
+
+namespace VNLib.Utils.Memory
+{
+ ///<summary>
+ ///<para>
+ /// Provides a win32 private heap managed wrapper class
+ ///</para>
+ ///</summary>
+ ///<remarks>
+ /// <see cref="PrivateHeap"/> implements <see cref="SafeHandle"/> and tracks allocated blocks by its
+ /// referrence counter. Allocations increment the count, and free's decrement the count, so the heap may
+ /// be disposed safely
+ /// </remarks>
+ [ComVisible(false)]
+ [SupportedOSPlatform("Windows")]
+ public sealed class PrivateHeap : UnmanagedHeapBase
+ {
+ private const string KERNEL_DLL = "Kernel32";
+
+ #region Extern
+ //Heap flags
+ public const DWORD HEAP_NO_FLAGS = 0x00;
+ public const DWORD HEAP_GENERATE_EXCEPTIONS = 0x04;
+ public const DWORD HEAP_NO_SERIALIZE = 0x01;
+ public const DWORD HEAP_REALLOC_IN_PLACE_ONLY = 0x10;
+ public const DWORD HEAP_ZERO_MEMORY = 0x08;
+
+ [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static extern LPVOID HeapAlloc(IntPtr hHeap, DWORD flags, SIZE_T dwBytes);
+ [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static extern LPVOID HeapReAlloc(IntPtr hHeap, DWORD dwFlags, LPVOID lpMem, SIZE_T dwBytes);
+ [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool HeapFree(IntPtr hHeap, DWORD dwFlags, LPVOID lpMem);
+
+ [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static extern LPVOID HeapCreate(DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize);
+ [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool HeapDestroy(IntPtr hHeap);
+ [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static extern bool HeapValidate(IntPtr hHeap, DWORD dwFlags, LPVOID lpMem);
+ [DllImport(KERNEL_DLL, SetLastError = true, ExactSpelling = true)]
+ [return: MarshalAs(UnmanagedType.U8)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static extern SIZE_T HeapSize(IntPtr hHeap, DWORD flags, LPVOID lpMem);
+
+ #endregion
+
+ /// <summary>
+ /// Create a new <see cref="PrivateHeap"/> with the specified sizes and flags
+ /// </summary>
+ /// <param name="initialSize">Intial size of the heap</param>
+ /// <param name="maxHeapSize">Maximum size allowed for the heap (disabled = 0, default)</param>
+ /// <param name="flags">Defalt heap flags to set globally for all blocks allocated by the heap (default = 0)</param>
+ public static PrivateHeap Create(SIZE_T initialSize, SIZE_T maxHeapSize = 0, DWORD flags = HEAP_NO_FLAGS)
+ {
+ //Call create, throw exception if the heap falled to allocate
+ IntPtr heapHandle = HeapCreate(flags, initialSize, maxHeapSize);
+ if (heapHandle == IntPtr.Zero)
+ {
+ throw new NativeMemoryException("Heap could not be created");
+ }
+#if TRACE
+ Trace.WriteLine($"Win32 private heap {heapHandle:x} created");
+#endif
+ //Heap has been created so we can wrap it
+ return new(heapHandle);
+ }
+ /// <summary>
+ /// LIFETIME WARNING. Consumes a valid win32 handle and will manage it's lifetime once constructed.
+ /// Locking and memory blocks will attempt to be allocated from this heap handle.
+ /// </summary>
+ /// <param name="win32HeapHandle">An open and valid handle to a win32 private heap</param>
+ /// <returns>A wrapper around the specified heap</returns>
+ public static PrivateHeap ConsumeExisting(IntPtr win32HeapHandle) => new (win32HeapHandle);
+
+ private PrivateHeap(IntPtr heapPtr) : base(false, true) => handle = heapPtr;
+
+ /// <summary>
+ /// Retrieves the size of a memory block allocated from the current heap.
+ /// </summary>
+ /// <param name="block">The pointer to a block of memory to get the size of</param>
+ /// <returns>The size of the block of memory, (SIZE_T)-1 if the operation fails</returns>
+ public SIZE_T HeapSize(ref LPVOID block) => HeapSize(handle, HEAP_NO_FLAGS, block);
+
+ /// <summary>
+ /// Validates the specified block of memory within the current heap instance. This function will block hte
+ /// </summary>
+ /// <param name="block">Pointer to the block of memory to validate</param>
+ /// <returns>True if the block is valid, false otherwise</returns>
+ public bool Validate(ref LPVOID block)
+ {
+ bool result;
+ //Lock the heap before validating
+ HeapLock.Wait();
+ //validate the block on the current heap
+ result = HeapValidate(handle, HEAP_NO_FLAGS, block);
+ //Unlock the heap
+ HeapLock.Release();
+ return result;
+
+ }
+ /// <summary>
+ /// Validates the current heap instance. The function scans all the memory blocks in the heap and verifies that the heap control structures maintained by
+ /// the heap manager are in a consistent state.
+ /// </summary>
+ /// <returns>If the specified heap or memory block is valid, the return value is nonzero.</returns>
+ /// <remarks>This can be a consuming operation which will block all allocations</remarks>
+ public bool Validate()
+ {
+ bool result;
+ //Lock the heap before validating
+ HeapLock.Wait();
+ //validate the entire heap
+ result = HeapValidate(handle, HEAP_NO_FLAGS, IntPtr.Zero);
+ //Unlock the heap
+ HeapLock.Release();
+ return result;
+ }
+
+ ///<inheritdoc/>
+ protected override bool ReleaseHandle()
+ {
+#if TRACE
+ Trace.WriteLine($"Win32 private heap {handle:x} destroyed");
+#endif
+ return HeapDestroy(handle) && base.ReleaseHandle();
+ }
+ ///<inheritdoc/>
+ protected override sealed LPVOID AllocBlock(ulong elements, ulong size, bool zero)
+ {
+ ulong bytes = checked(elements * size);
+ return HeapAlloc(handle, zero ? HEAP_ZERO_MEMORY : HEAP_NO_FLAGS, bytes);
+ }
+ ///<inheritdoc/>
+ protected override sealed bool FreeBlock(LPVOID block) => HeapFree(handle, HEAP_NO_FLAGS, block);
+ ///<inheritdoc/>
+ protected override sealed LPVOID ReAllocBlock(LPVOID block, ulong elements, ulong size, bool zero)
+ {
+ ulong bytes = checked(elements * size);
+ return HeapReAlloc(handle, zero ? HEAP_ZERO_MEMORY : HEAP_NO_FLAGS, block, bytes);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/PrivateString.cs b/lib/Utils/src/Memory/PrivateString.cs
new file mode 100644
index 0000000..20d658a
--- /dev/null
+++ b/lib/Utils/src/Memory/PrivateString.cs
@@ -0,0 +1,183 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: PrivateString.cs
+*
+* PrivateString.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Provides a wrapper class that will have unsafe access to the memory of
+ /// the specified <see cref="string"/> provided during object creation.
+ /// </summary>
+ /// <remarks>The value of the memory the protected string points to is undefined when the instance is disposed</remarks>
+ public class PrivateString : PrivateStringManager, IEquatable<PrivateString>, IEquatable<string>, ICloneable
+ {
+ protected string StrRef => base[0]!;
+ private readonly bool OwnsReferrence;
+
+ /// <summary>
+ /// Creates a new <see cref="PrivateString"/> over the specified string and the memory it points to.
+ /// </summary>
+ /// <param name="data">The <see cref="string"/> instance pointing to the memory to protect</param>
+ /// <param name="ownsReferrence">Does the current instance "own" the memory the data parameter points to</param>
+ /// <remarks>You should no longer reference the input string directly</remarks>
+ public PrivateString(string data, bool ownsReferrence = true) : base(1)
+ {
+ //Create a private string manager to store referrence to string
+ base[0] = data ?? throw new ArgumentNullException(nameof(data));
+ OwnsReferrence = ownsReferrence;
+ }
+
+ //Create private string from a string
+ public static explicit operator PrivateString?(string? data)
+ {
+ //Allow passing null strings during implicit casting
+ return data == null ? null : new(data);
+ }
+
+ public static PrivateString? ToPrivateString(string? value)
+ {
+ return value == null ? null : new PrivateString(value, true);
+ }
+
+ //Cast to string
+ public static explicit operator string (PrivateString str)
+ {
+ //Check if disposed, or return the string
+ str.Check();
+ return str.StrRef;
+ }
+
+ public static implicit operator ReadOnlySpan<char>(PrivateString str)
+ {
+ return str.Disposed ? Span<char>.Empty : str.StrRef.AsSpan();
+ }
+
+ /// <summary>
+ /// Gets the value of the internal string as a <see cref="ReadOnlySpan{T}"/>
+ /// </summary>
+ /// <returns>The <see cref="ReadOnlySpan{T}"/> referrence to the internal string</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public ReadOnlySpan<char> ToReadOnlySpan()
+ {
+ Check();
+ return StrRef.AsSpan();
+ }
+
+ ///<inheritdoc/>
+ public bool Equals(string? other)
+ {
+ Check();
+ return StrRef.Equals(other, StringComparison.Ordinal);
+ }
+ ///<inheritdoc/>
+ public bool Equals(PrivateString? other)
+ {
+ Check();
+ return other != null && StrRef.Equals(other.StrRef, StringComparison.Ordinal);
+ }
+ ///<inheritdoc/>
+ public override bool Equals(object? obj)
+ {
+ Check();
+ return obj is PrivateString otherRef && StrRef.Equals(otherRef);
+ }
+ ///<inheritdoc/>
+ public bool Equals(ReadOnlySpan<char> other)
+ {
+ Check();
+ return StrRef.AsSpan().SequenceEqual(other);
+ }
+ /// <summary>
+ /// Creates a deep copy of the internal string and returns that copy
+ /// </summary>
+ /// <returns>A deep copy of the internal string</returns>
+ public override string ToString()
+ {
+ Check();
+ return new(StrRef.AsSpan());
+ }
+ /// <summary>
+ /// String length
+ /// </summary>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public int Length
+ {
+ get
+ {
+ Check();
+ return StrRef.Length;
+ }
+ }
+ /// <summary>
+ /// Indicates whether the underlying string is null or an empty string ("")
+ /// </summary>
+ /// <param name="ps"></param>
+ /// <returns>True if the parameter is null, or an empty string (""). False otherwise</returns>
+ public static bool IsNullOrEmpty([NotNullWhen(false)] PrivateString? ps) => ps == null|| ps.Length == 0;
+
+ /// <summary>
+ /// The hashcode of the underlying string
+ /// </summary>
+ /// <returns></returns>
+ public override int GetHashCode()
+ {
+ Check();
+ return StrRef.GetHashCode(StringComparison.Ordinal);
+ }
+
+ /// <summary>
+ /// Creates a new deep copy of the current instance that is an independent <see cref="PrivateString"/>
+ /// </summary>
+ /// <returns>The new <see cref="PrivateString"/> instance</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public override object Clone()
+ {
+ Check();
+ //Copy all contents of string to another reference
+ string clone = new (StrRef.AsSpan());
+ //return a new private string
+ return new PrivateString(clone, true);
+ }
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ Erase();
+ }
+
+ /// <summary>
+ /// Erases the contents of the internal CLR string
+ /// </summary>
+ public void Erase()
+ {
+ //Only dispose the instance if we own the memory
+ if (OwnsReferrence && !Disposed)
+ {
+ base.Free();
+ }
+ }
+ }
+}
diff --git a/lib/Utils/src/Memory/PrivateStringManager.cs b/lib/Utils/src/Memory/PrivateStringManager.cs
new file mode 100644
index 0000000..9ed8f5f
--- /dev/null
+++ b/lib/Utils/src/Memory/PrivateStringManager.cs
@@ -0,0 +1,117 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: PrivateStringManager.cs
+*
+* PrivateStringManager.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// When inherited by a class, provides a safe string storage that zeros a CLR string memory on disposal
+ /// </summary>
+ public class PrivateStringManager : VnDisposeable, ICloneable
+ {
+ /// <summary>
+ /// Strings to be cleared when exiting
+ /// </summary>
+ private readonly string?[] ProtectedElements;
+ /// <summary>
+ /// Gets or sets a string referrence into the protected elements store
+ /// </summary>
+ /// <param name="index"></param>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <returns>Referrence to string associated with the index</returns>
+ protected string? this[int index]
+ {
+ get
+ {
+ Check();
+ return ProtectedElements[index];
+ }
+ set
+ {
+ Check();
+ //Check to see if the string has been interned
+ if (!string.IsNullOrEmpty(value) && string.IsInterned(value) != null)
+ {
+ throw new ArgumentException($"The specified string has been CLR interned and cannot be stored in {nameof(PrivateStringManager)}");
+ }
+ //Clear the old value before setting the new one
+ if (!string.IsNullOrEmpty(ProtectedElements[index]))
+ {
+ Memory.UnsafeZeroMemory<char>(ProtectedElements[index]);
+ }
+ //set new value
+ ProtectedElements[index] = value;
+ }
+ }
+ /// <summary>
+ /// Create a new instance with fixed array size
+ /// </summary>
+ /// <param name="elements">Number of elements to protect</param>
+ public PrivateStringManager(int elements)
+ {
+ //Allocate the string array
+ ProtectedElements = new string[elements];
+ }
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ //Zero all strings specified
+ for (int i = 0; i < ProtectedElements.Length; i++)
+ {
+ if (!string.IsNullOrEmpty(ProtectedElements[i]))
+ {
+ //Zero the string memory
+ Memory.UnsafeZeroMemory<char>(ProtectedElements[i]);
+ //Set to null
+ ProtectedElements[i] = null;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Creates a deep copy for a new independent <see cref="PrivateStringManager"/>
+ /// </summary>
+ /// <returns>A new independent <see cref="PrivateStringManager"/> instance</returns>
+ /// <remarks>Be careful duplicating large instances, and make sure clones are properly disposed if necessary</remarks>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public virtual object Clone()
+ {
+ Check();
+ PrivateStringManager other = new (ProtectedElements.Length);
+ //Copy all strings to the other instance
+ for(int i = 0; i < ProtectedElements.Length; i++)
+ {
+ //Copy all strings and store their copies in the new array
+ other.ProtectedElements[i] = this.ProtectedElements[i].AsSpan().ToString();
+ }
+ //return the new copy
+ return other;
+ }
+ }
+}
diff --git a/lib/Utils/src/Memory/ProcessHeap.cs b/lib/Utils/src/Memory/ProcessHeap.cs
new file mode 100644
index 0000000..4f06d52
--- /dev/null
+++ b/lib/Utils/src/Memory/ProcessHeap.cs
@@ -0,0 +1,82 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ProcessHeap.cs
+*
+* ProcessHeap.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Provides a <see cref="IUnmangedHeap"/> wrapper for the <see cref="Marshal"/> virtualalloc
+ /// global heap methods
+ /// </summary>
+ [ComVisible(false)]
+ public unsafe class ProcessHeap : VnDisposeable, IUnmangedHeap
+ {
+ /// <summary>
+ /// Initalizes a new global (cross platform) process heap
+ /// </summary>
+ public ProcessHeap()
+ {
+#if TRACE
+ Trace.WriteLine($"Default heap instnace created {GetHashCode():x}");
+#endif
+ }
+
+ ///<inheritdoc/>
+ ///<exception cref="OverflowException"></exception>
+ ///<exception cref="OutOfMemoryException"></exception>
+ public IntPtr Alloc(ulong elements, ulong size, bool zero)
+ {
+ return zero
+ ? (IntPtr)NativeMemory.AllocZeroed((nuint)elements, (nuint)size)
+ : (IntPtr)NativeMemory.Alloc((nuint)elements, (nuint)size);
+ }
+ ///<inheritdoc/>
+ public bool Free(ref IntPtr block)
+ {
+ //Free native mem from ptr
+ NativeMemory.Free(block.ToPointer());
+ block = IntPtr.Zero;
+ return true;
+ }
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+#if TRACE
+ Trace.WriteLine($"Default heap instnace disposed {GetHashCode():x}");
+#endif
+ }
+ ///<inheritdoc/>
+ ///<exception cref="OverflowException"></exception>
+ ///<exception cref="OutOfMemoryException"></exception>
+ public void Resize(ref IntPtr block, ulong elements, ulong size, bool zero)
+ {
+ nuint bytes = checked((nuint)(elements * size));
+ IntPtr old = block;
+ block = (IntPtr)NativeMemory.Realloc(old.ToPointer(), bytes);
+ }
+ }
+}
diff --git a/lib/Utils/src/Memory/RpMallocPrivateHeap.cs b/lib/Utils/src/Memory/RpMallocPrivateHeap.cs
new file mode 100644
index 0000000..8ed79b6
--- /dev/null
+++ b/lib/Utils/src/Memory/RpMallocPrivateHeap.cs
@@ -0,0 +1,279 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: RpMallocPrivateHeap.cs
+*
+* RpMallocPrivateHeap.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+
+using size_t = System.UInt64;
+using LPVOID = System.IntPtr;
+using LPHEAPHANDLE = System.IntPtr;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// A wrapper class for cross platform RpMalloc implementation.
+ /// </summary>
+ [ComVisible(false)]
+ public sealed class RpMallocPrivateHeap : UnmanagedHeapBase
+ {
+ const string DLL_NAME = "rpmalloc";
+
+ #region statics
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern int rpmalloc_initialize();
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern void rpmalloc_finalize();
+
+ //Heap api
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern LPHEAPHANDLE rpmalloc_heap_acquire();
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern void rpmalloc_heap_release(LPHEAPHANDLE heap);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern LPVOID rpmalloc_heap_alloc(LPHEAPHANDLE heap, size_t size);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern LPVOID rpmalloc_heap_aligned_alloc(LPHEAPHANDLE heap, size_t alignment, size_t size);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern LPVOID rpmalloc_heap_calloc(LPHEAPHANDLE heap, size_t num, size_t size);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern LPVOID rpmalloc_heap_aligned_calloc(LPHEAPHANDLE heap, size_t alignment, size_t num, size_t size);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern LPVOID rpmalloc_heap_realloc(LPHEAPHANDLE heap, LPVOID ptr, size_t size, nuint flags);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern LPVOID rpmalloc_heap_aligned_realloc(LPHEAPHANDLE heap, LPVOID ptr, size_t alignment, size_t size, nuint flags);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern void rpmalloc_heap_free(LPHEAPHANDLE heap, LPVOID ptr);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern void rpmalloc_heap_free_all(LPHEAPHANDLE heap);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern void rpmalloc_heap_thread_set_current(LPHEAPHANDLE heap);
+
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern void rpmalloc_thread_initialize();
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern int rpmalloc_is_thread_initialized();
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern void rpmalloc_thread_finalize(int release_caches);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern LPVOID rpmalloc(size_t size);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern LPVOID rpcalloc(size_t num, size_t size);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern LPVOID rprealloc(LPVOID ptr, size_t size);
+ [DllImport(DLL_NAME, ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ static extern void rpfree(LPVOID ptr);
+
+ #endregion
+
+ private sealed class RpMallocGlobalHeap : IUnmangedHeap
+ {
+ IntPtr IUnmangedHeap.Alloc(ulong elements, ulong size, bool zero)
+ {
+ return RpMalloc(elements, (nuint)size, zero);
+ }
+
+ //Global heap does not need to be disposed
+ void IDisposable.Dispose()
+ { }
+
+ bool IUnmangedHeap.Free(ref IntPtr block)
+ {
+ //Free the block
+ RpFree(ref block);
+ return true;
+ }
+
+ void IUnmangedHeap.Resize(ref IntPtr block, ulong elements, ulong size, bool zero)
+ {
+ //Try to resize the block
+ IntPtr resize = RpRealloc(block, elements, (nuint)size);
+
+ if (resize == IntPtr.Zero)
+ {
+ throw new NativeMemoryOutOfMemoryException("Failed to resize the block");
+ }
+ //assign ptr
+ block = resize;
+ }
+ }
+
+ /// <summary>
+ /// <para>
+ /// A <see cref="IUnmangedHeap"/> API for the RPMalloc library if loaded.
+ /// </para>
+ /// <para>
+ /// This heap is thread safe and may be converted to a <see cref="MemoryManager{T}"/>
+ /// infinitley and disposed safely.
+ /// </para>
+ /// <para>
+ /// If the native library is not loaded, calls to this API will throw a <see cref="DllNotFoundException"/>.
+ /// </para>
+ /// </summary>
+ public static IUnmangedHeap GlobalHeap { get; } = new RpMallocGlobalHeap();
+
+ /// <summary>
+ /// <para>
+ /// Initializes RpMalloc for the current thread and alloctes a block of memory
+ /// </para>
+ /// </summary>
+ /// <param name="elements">The number of elements to allocate</param>
+ /// <param name="size">The number of bytes per element type (aligment)</param>
+ /// <param name="zero">Zero the block of memory before returning</param>
+ /// <returns>A pointer to the block, (zero if failed)</returns>
+ public static LPVOID RpMalloc(size_t elements, nuint size, bool zero)
+ {
+ //See if the current thread has been initialized
+ if (rpmalloc_is_thread_initialized() == 0)
+ {
+ //Initialize the current thread
+ rpmalloc_thread_initialize();
+ }
+ //Alloc block
+ LPVOID block;
+ if (zero)
+ {
+ block = rpcalloc(elements, size);
+ }
+ else
+ {
+ //Calculate the block size
+ ulong blockSize = checked(elements * size);
+ block = rpmalloc(blockSize);
+ }
+ return block;
+ }
+
+ /// <summary>
+ /// Frees a block of memory allocated by RpMalloc
+ /// </summary>
+ /// <param name="block">A ref to the pointer of the block to free</param>
+ public static void RpFree(ref LPVOID block)
+ {
+ if (block != IntPtr.Zero)
+ {
+ rpfree(block);
+ block = IntPtr.Zero;
+ }
+ }
+
+ /// <summary>
+ /// Attempts to re-allocate the specified block on the global heap
+ /// </summary>
+ /// <param name="block">A pointer to a previously allocated block of memory</param>
+ /// <param name="elements">The number of elements in the block</param>
+ /// <param name="size">The number of bytes in the element</param>
+ /// <returns>A pointer to the new block if the reallocation succeeded, null if the resize failed</returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="OverflowException"></exception>
+ public static LPVOID RpRealloc(LPVOID block, size_t elements, nuint size)
+ {
+ if(block == IntPtr.Zero)
+ {
+ throw new ArgumentException("The supplied block is not valid", nameof(block));
+ }
+ //Calc new block size
+ size_t blockSize = checked(elements * size);
+ return rprealloc(block, blockSize);
+ }
+
+ #region instance
+
+ /// <summary>
+ /// Initializes a new RpMalloc first class heap to allocate memory blocks from
+ /// </summary>
+ /// <param name="zeroAll">A global flag to zero all blocks of memory allocated</param>
+ /// <exception cref="NativeMemoryOutOfMemoryException"></exception>
+ public RpMallocPrivateHeap(bool zeroAll):base(zeroAll, true)
+ {
+ //Alloc the heap
+ handle = rpmalloc_heap_acquire();
+ if(IsInvalid)
+ {
+ throw new NativeMemoryException("Failed to aquire a new heap");
+ }
+#if TRACE
+ Trace.WriteLine($"RPMalloc heap {handle:x} created");
+#endif
+ }
+ ///<inheritdoc/>
+ protected override bool ReleaseHandle()
+ {
+#if TRACE
+ Trace.WriteLine($"RPMalloc heap {handle:x} destroyed");
+#endif
+ //Release all heap memory
+ rpmalloc_heap_free_all(handle);
+ //Destroy the heap
+ rpmalloc_heap_release(handle);
+ //Release base
+ return base.ReleaseHandle();
+ }
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected sealed override LPVOID AllocBlock(ulong elements, ulong size, bool zero)
+ {
+ //Alloc or calloc and initalize
+ return zero ? rpmalloc_heap_calloc(handle, elements, size) : rpmalloc_heap_alloc(handle, checked(size * elements));
+ }
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected sealed override bool FreeBlock(LPVOID block)
+ {
+ //Free block
+ rpmalloc_heap_free(handle, block);
+ return true;
+ }
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected sealed override LPVOID ReAllocBlock(LPVOID block, ulong elements, ulong size, bool zero)
+ {
+ //Realloc
+ return rpmalloc_heap_realloc(handle, block, checked(elements * size), 0);
+ }
+ #endregion
+ }
+}
diff --git a/lib/Utils/src/Memory/SubSequence.cs b/lib/Utils/src/Memory/SubSequence.cs
new file mode 100644
index 0000000..3800fb5
--- /dev/null
+++ b/lib/Utils/src/Memory/SubSequence.cs
@@ -0,0 +1,113 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: SubSequence.cs
+*
+* SubSequence.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Represents a subset (or window) of data within a <see cref="MemoryHandle{T}"/>
+ /// </summary>
+ /// <typeparam name="T">The unmanaged type to wrap</typeparam>
+ public readonly struct SubSequence<T> : IEquatable<SubSequence<T>> where T: unmanaged
+ {
+ private readonly MemoryHandle<T> _handle;
+ /// <summary>
+ /// The number of elements in the current window
+ /// </summary>
+ public readonly int Size { get; }
+
+ /// <summary>
+ /// Creates a new <see cref="SubSequence{T}"/> to the handle to get a window of the block
+ /// </summary>
+ /// <param name="block"></param>
+ /// <param name="offset"></param>
+ /// <param name="size"></param>
+#if TARGET_64_BIT
+ public SubSequence(MemoryHandle<T> block, ulong offset, int size)
+#else
+ public SubSequence(MemoryHandle<T> block, int offset, int size)
+#endif
+ {
+ _offset = offset;
+ Size = size >= 0 ? size : throw new ArgumentOutOfRangeException(nameof(size));
+ _handle = block ?? throw new ArgumentNullException(nameof(block));
+ }
+
+
+#if TARGET_64_BIT
+ private readonly ulong _offset;
+#else
+ private readonly int _offset;
+#endif
+ /// <summary>
+ /// Gets a <see cref="Span{T}"/> that is offset from the base of the handle
+ /// </summary>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+
+#if TARGET_64_BIT
+ public readonly Span<T> Span => Size > 0 ? _handle.GetOffsetSpan(_offset, Size) : Span<T>.Empty;
+#else
+ public readonly Span<T> Span => Size > 0 ? _handle.Span.Slice(_offset, Size) : Span<T>.Empty;
+#endif
+
+ /// <summary>
+ /// Slices the current sequence into a smaller <see cref="SubSequence{T}"/>
+ /// </summary>
+ /// <param name="offset">The relative offset from the current window offset</param>
+ /// <param name="size">The size of the block</param>
+ /// <returns>A <see cref="SubSequence{T}"/> of the current sequence</returns>
+ public readonly SubSequence<T> Slice(uint offset, int size) => new (_handle, _offset + checked((int)offset), size);
+
+ /// <summary>
+ /// Returns the signed 32-bit hashcode
+ /// </summary>
+ /// <returns>A signed 32-bit integer that represents the hashcode for the current instance</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public readonly override int GetHashCode() => _handle.GetHashCode() + _offset.GetHashCode();
+
+ ///<inheritdoc/>
+ public readonly bool Equals(SubSequence<T> other) => Span.SequenceEqual(other.Span);
+
+ ///<inheritdoc/>
+ public readonly override bool Equals(object? obj) => obj is SubSequence<T> other && Equals(other);
+
+ /// <summary>
+ /// Determines if two <see cref="SubSequence{T}"/> are equal
+ /// </summary>
+ /// <param name="left"></param>
+ /// <param name="right"></param>
+ /// <returns>True if the sequences are equal, false otherwise</returns>
+ public static bool operator ==(SubSequence<T> left, SubSequence<T> right) => left.Equals(right);
+ /// <summary>
+ /// Determines if two <see cref="SubSequence{T}"/> are not equal
+ /// </summary>
+ /// <param name="left"></param>
+ /// <param name="right"></param>
+ /// <returns>True if the sequences are not equal, false otherwise</returns>
+ public static bool operator !=(SubSequence<T> left, SubSequence<T> right) => !left.Equals(right);
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/SysBufferMemoryManager.cs b/lib/Utils/src/Memory/SysBufferMemoryManager.cs
new file mode 100644
index 0000000..040467f
--- /dev/null
+++ b/lib/Utils/src/Memory/SysBufferMemoryManager.cs
@@ -0,0 +1,102 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: SysBufferMemoryManager.cs
+*
+* SysBufferMemoryManager.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Provides an unmanaged System.Buffers integration with zero-cost pinning. Uses <see cref="MemoryHandle{T}"/>
+ /// as a memory provider which implements a <see cref="System.Runtime.InteropServices.SafeHandle"/>
+ /// </summary>
+ /// <typeparam name="T">Unmanaged memory type</typeparam>
+ public sealed class SysBufferMemoryManager<T> : MemoryManager<T> where T :unmanaged
+ {
+ private readonly IMemoryHandle<T> BackingMemory;
+ private readonly bool _ownsHandle;
+
+ /// <summary>
+ /// Consumes an exisitng <see cref="MemoryHandle{T}"/> to provide <see cref="Memory"/> wrappers.
+ /// The handle should no longer be referrenced directly
+ /// </summary>
+ /// <param name="existingHandle">The existing handle to consume</param>
+ /// <param name="ownsHandle">A value that indicates if the memory manager owns the handle reference</param>
+ internal SysBufferMemoryManager(IMemoryHandle<T> existingHandle, bool ownsHandle)
+ {
+ BackingMemory = existingHandle;
+ _ownsHandle = ownsHandle;
+ }
+
+ /// <summary>
+ /// Allocates a fized size buffer from the specified unmanaged <see cref="PrivateHeap"/>
+ /// </summary>
+ /// <param name="heap">The heap to perform allocations from</param>
+ /// <param name="elements">The number of elements to allocate</param>
+ /// <param name="zero">Zero allocations</param>
+ public SysBufferMemoryManager(IUnmangedHeap heap, ulong elements, bool zero)
+ {
+ BackingMemory = heap.Alloc<T>(elements, zero);
+ _ownsHandle = true;
+ }
+
+ ///<inheritdoc/>
+ protected override bool TryGetArray(out ArraySegment<T> segment)
+ {
+ //Always false since no array is available
+ segment = default;
+ return false;
+ }
+
+ ///<inheritdoc/>
+ ///<exception cref="OverflowException"></exception>
+ ///<exception cref="ObjectDisposedException"></exception>
+ public override Span<T> GetSpan() => BackingMemory.Span;
+
+ /// <summary>
+ /// <inheritdoc/>
+ /// </summary>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public unsafe override MemoryHandle Pin(int elementIndex = 0)
+ {
+ return BackingMemory.Pin(elementIndex);
+ }
+
+ ///<inheritdoc/>
+ public override void Unpin()
+ {}
+
+ ///<inheritdoc/>
+ protected override void Dispose(bool disposing)
+ {
+ if (_ownsHandle)
+ {
+ BackingMemory.Dispose();
+ }
+ }
+ }
+}
diff --git a/lib/Utils/src/Memory/UnmanagedHeapBase.cs b/lib/Utils/src/Memory/UnmanagedHeapBase.cs
new file mode 100644
index 0000000..5c92aff
--- /dev/null
+++ b/lib/Utils/src/Memory/UnmanagedHeapBase.cs
@@ -0,0 +1,185 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: UnmanagedHeapBase.cs
+*
+* UnmanagedHeapBase.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+using System.Runtime.InteropServices;
+
+using Microsoft.Win32.SafeHandles;
+
+using size_t = System.UInt64;
+using LPVOID = System.IntPtr;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Provides a synchronized base methods for accessing unmanaged memory. Implements <see cref="SafeHandle"/>
+ /// for safe disposal of heaps
+ /// </summary>
+ public abstract class UnmanagedHeapBase : SafeHandleZeroOrMinusOneIsInvalid, IUnmangedHeap
+ {
+ /// <summary>
+ /// The heap synchronization handle
+ /// </summary>
+ protected readonly SemaphoreSlim HeapLock;
+ /// <summary>
+ /// The global heap zero flag
+ /// </summary>
+ protected readonly bool GlobalZero;
+
+ /// <summary>
+ /// Initalizes the unmanaged heap base class (init synchronization handle)
+ /// </summary>
+ /// <param name="globalZero">A global flag to zero all blocks of memory during allocation</param>
+ /// <param name="ownsHandle">A flag that indicates if the handle is owned by the instance</param>
+ protected UnmanagedHeapBase(bool globalZero, bool ownsHandle) : base(ownsHandle)
+ {
+ HeapLock = new(1, 1);
+ GlobalZero = globalZero;
+ }
+
+ ///<inheritdoc/>
+ ///<remarks>Increments the handle count</remarks>
+ ///<exception cref="OutOfMemoryException"></exception>
+ ///<exception cref="ObjectDisposedException"></exception>
+ public LPVOID Alloc(size_t elements, size_t size, bool zero)
+ {
+ //Force zero if global flag is set
+ zero |= GlobalZero;
+ bool handleCountIncremented = false;
+ //Increment handle count to prevent premature release
+ DangerousAddRef(ref handleCountIncremented);
+ //Failed to increment ref count, class has been disposed
+ if (!handleCountIncremented)
+ {
+ throw new ObjectDisposedException("The handle has been released");
+ }
+ try
+ {
+ //wait for lock
+ HeapLock.Wait();
+ //Alloc block
+ LPVOID block = AllocBlock(elements, size, zero);
+ //release lock
+ HeapLock.Release();
+ //Check if block was allocated
+ return block != IntPtr.Zero ? block : throw new NativeMemoryOutOfMemoryException("Failed to allocate the requested block");
+ }
+ catch
+ {
+ //Decrement handle count since allocation failed
+ DangerousRelease();
+ throw;
+ }
+ }
+ ///<inheritdoc/>
+ ///<remarks>Decrements the handle count</remarks>
+ public bool Free(ref LPVOID block)
+ {
+ bool result;
+ //If disposed, set the block handle to zero and exit to avoid raising exceptions during finalization
+ if (IsClosed || IsInvalid)
+ {
+ block = IntPtr.Zero;
+ return true;
+ }
+ //wait for lock
+ HeapLock.Wait();
+ //Free block
+ result = FreeBlock(block);
+ //Release lock before releasing handle
+ HeapLock.Release();
+ //Decrement handle count
+ DangerousRelease();
+ //set block to invalid
+ block = IntPtr.Zero;
+ return result;
+ }
+ ///<inheritdoc/>
+ ///<exception cref="OutOfMemoryException"></exception>
+ ///<exception cref="ObjectDisposedException"></exception>
+ public void Resize(ref LPVOID block, size_t elements, size_t size, bool zero)
+ {
+ //wait for lock
+ HeapLock.Wait();
+ /*
+ * Realloc may return a null pointer if allocation fails
+ * so check the results and only assign the block pointer
+ * if the result is valid. Otherwise pointer block should
+ * be left untouched
+ */
+ LPVOID newBlock = ReAllocBlock(block, elements, size, zero);
+ //release lock
+ HeapLock.Release();
+ //Check block
+ if (newBlock == IntPtr.Zero)
+ {
+ throw new NativeMemoryOutOfMemoryException("The memory block could not be resized");
+ }
+ //Set the new block
+ block = newBlock;
+ }
+
+ ///<inheritdoc/>
+ protected override bool ReleaseHandle()
+ {
+ HeapLock.Dispose();
+ return true;
+ }
+
+ /// <summary>
+ /// Allocates a block of memory from the heap
+ /// </summary>
+ /// <param name="elements">The number of elements within the block</param>
+ /// <param name="size">The size of the element type (in bytes)</param>
+ /// <param name="zero">A flag to zero the allocated block</param>
+ /// <returns>A pointer to the allocated block</returns>
+ protected abstract LPVOID AllocBlock(size_t elements, size_t size, bool zero);
+ /// <summary>
+ /// Frees a previously allocated block of memory
+ /// </summary>
+ /// <param name="block">The block to free</param>
+ protected abstract bool FreeBlock(LPVOID block);
+ /// <summary>
+ /// Resizes the previously allocated block of memory on the current heap
+ /// </summary>
+ /// <param name="block">The prevously allocated block</param>
+ /// <param name="elements">The new number of elements within the block</param>
+ /// <param name="size">The size of the element type (in bytes)</param>
+ /// <param name="zero">A flag to indicate if the new region of the block should be zeroed</param>
+ /// <returns>A pointer to the same block, but resized, null if the allocation falied</returns>
+ /// <remarks>
+ /// Heap base relies on the block pointer to remain unchanged if the resize fails so the
+ /// block is still valid, and the return value is used to determine if the resize was successful
+ /// </remarks>
+ protected abstract LPVOID ReAllocBlock(LPVOID block, size_t elements, size_t size, bool zero);
+ ///<inheritdoc/>
+ public override int GetHashCode() => handle.GetHashCode();
+ ///<inheritdoc/>
+ public override bool Equals(object? obj)
+ {
+ return obj is UnmanagedHeapBase heap && !heap.IsInvalid && !heap.IsClosed && handle == heap.handle;
+ }
+ }
+}
diff --git a/lib/Utils/src/Memory/UnsafeMemoryHandle.cs b/lib/Utils/src/Memory/UnsafeMemoryHandle.cs
new file mode 100644
index 0000000..b05ad40
--- /dev/null
+++ b/lib/Utils/src/Memory/UnsafeMemoryHandle.cs
@@ -0,0 +1,231 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: UnsafeMemoryHandle.cs
+*
+* UnsafeMemoryHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+using System.Diagnostics.CodeAnalysis;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Represents an unsafe handle to managed/unmanaged memory that should be used cautiously.
+ /// A referrence counter is not maintained.
+ /// </summary>
+ /// <typeparam name="T">Unmanaged memory type</typeparam>
+ [StructLayout(LayoutKind.Sequential)]
+ public readonly struct UnsafeMemoryHandle<T> : IMemoryHandle<T>, IEquatable<UnsafeMemoryHandle<T>> where T : unmanaged
+ {
+ private enum HandleType
+ {
+ None,
+ Pool,
+ PrivateHeap
+ }
+
+ private readonly T[]? _poolArr;
+ private readonly IntPtr _memoryPtr;
+ private readonly ArrayPool<T>? _pool;
+ private readonly IUnmangedHeap? _heap;
+ private readonly HandleType _handleType;
+ private readonly int _length;
+
+ ///<inheritdoc/>
+ public readonly unsafe Span<T> Span
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _handleType == HandleType.Pool ? _poolArr.AsSpan(0, IntLength) : new (_memoryPtr.ToPointer(), IntLength);
+ }
+ ///<inheritdoc/>
+ public readonly int IntLength => _length;
+ ///<inheritdoc/>
+ public readonly ulong Length => (ulong)_length;
+
+ /// <summary>
+ /// Creates an empty <see cref="UnsafeMemoryHandle{T}"/>
+ /// </summary>
+ public UnsafeMemoryHandle()
+ {
+ _pool = null;
+ _heap = null;
+ _poolArr = null;
+ _memoryPtr = IntPtr.Zero;
+ _handleType = HandleType.None;
+ _length = 0;
+ }
+
+ /// <summary>
+ /// Inializes a new <see cref="UnsafeMemoryHandle{T}"/> using the specified
+ /// <see cref="ArrayPool{T}"/>
+ /// </summary>
+ /// <param name="elements">The number of elements to store</param>
+ /// <param name="zero">Zero initial contents?</param>
+ /// <param name="pool">The explicit pool to alloc buffers from</param>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public unsafe UnsafeMemoryHandle(ArrayPool<T> pool, int elements, bool zero)
+ {
+ if (elements < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(elements));
+ }
+ //Pool is required
+ _pool = pool ?? throw new ArgumentNullException(nameof(pool));
+ //Rent the array from the pool and hold referrence to it
+ _poolArr = pool.Rent(elements, zero);
+ //Cant store ref to array becase GC can move it
+ _memoryPtr = IntPtr.Zero;
+ //Set pool handle type
+ _handleType = HandleType.Pool;
+ //No heap being loaded
+ _heap = null;
+ _length = elements;
+ }
+
+ /// <summary>
+ /// Intializes a new <see cref="UnsafeMemoryHandle{T}"/> for block of memory allocated from
+ /// an <see cref="IUnmangedHeap"/>
+ /// </summary>
+ /// <param name="heap">The heap the initial memory block belongs to</param>
+ /// <param name="initial">A pointer to the unmanaged memory block</param>
+ /// <param name="elements">The number of elements this block points to</param>
+ internal UnsafeMemoryHandle(IUnmangedHeap heap, IntPtr initial, int elements)
+ {
+ _pool = null;
+ _poolArr = null;
+ _heap = heap;
+ _length = elements;
+ _memoryPtr = initial;
+ _handleType = HandleType.PrivateHeap;
+ }
+
+ /// <summary>
+ /// Releases memory back to the pool or heap from which is was allocated.
+ /// </summary>
+ /// <remarks>After this method is called, this handle points to invalid memory</remarks>
+ public readonly void Dispose()
+ {
+ switch (_handleType)
+ {
+ case HandleType.Pool:
+ {
+ //Return array to pool
+ _pool!.Return(_poolArr!);
+ }
+ break;
+ case HandleType.PrivateHeap:
+ {
+ IntPtr unalloc = _memoryPtr;
+ //Free the unmanaged handle
+ _heap!.Free(ref unalloc);
+ }
+ break;
+ }
+ }
+
+ ///<inheritdoc/>
+ public readonly override int GetHashCode() => _handleType == HandleType.Pool ? _poolArr!.GetHashCode() : _memoryPtr.GetHashCode();
+ ///<inheritdoc/>
+ public readonly unsafe MemoryHandle Pin(int elementIndex)
+ {
+ //Guard
+ if (elementIndex < 0 || elementIndex >= IntLength)
+ {
+ throw new ArgumentOutOfRangeException(nameof(elementIndex));
+ }
+
+ if (_handleType == HandleType.Pool)
+ {
+ //Pin the array
+ GCHandle arrHandle = GCHandle.Alloc(_poolArr, GCHandleType.Pinned);
+ //Get array base address
+ void* basePtr = (void*)arrHandle.AddrOfPinnedObject();
+ //Get element offset
+ void* indexOffet = Unsafe.Add<T>(basePtr, elementIndex);
+ return new (indexOffet, arrHandle);
+ }
+ else
+ {
+ //Get offset pointer and pass self as pinnable argument, (nothing happens but support it)
+ void* basePtr = Unsafe.Add<T>(_memoryPtr.ToPointer(), elementIndex);
+ //Unmanaged memory is always pinned, so no need to pass this as IPinnable, since it will cause a box
+ return new (basePtr);
+ }
+ }
+ ///<inheritdoc/>
+ public readonly void Unpin()
+ {
+ //Nothing to do since gc handle takes care of array, and unmanaged pointers are not pinned
+ }
+
+ /// <summary>
+ /// Determines if the other handle represents the same memory block as the
+ /// current handle.
+ /// </summary>
+ /// <param name="other">The other handle to test</param>
+ /// <returns>True if the other handle points to the same block of memory as the current handle</returns>
+ public readonly bool Equals(UnsafeMemoryHandle<T> other)
+ {
+ return _handleType == other._handleType && Length == other.Length && GetHashCode() == other.GetHashCode();
+ }
+
+ /// <summary>
+ /// Override for object equality operator, will cause boxing
+ /// for structures
+ /// </summary>
+ /// <param name="obj">The other object to compare</param>
+ /// <returns>
+ /// True if the passed object is of type <see cref="UnsafeMemoryHandle{T}"/>
+ /// and uses the structure equality operator
+ /// false otherwise.
+ /// </returns>
+ public readonly override bool Equals([NotNullWhen(true)] object? obj) => obj is UnsafeMemoryHandle<T> other && Equals(other);
+
+ /// <summary>
+ /// Casts the handle to it's <see cref="Span{T}"/> representation
+ /// </summary>
+ /// <param name="handle">the handle to cast</param>
+ public static implicit operator Span<T>(in UnsafeMemoryHandle<T> handle) => handle.Span;
+
+ /// <summary>
+ /// Equality overload
+ /// </summary>
+ /// <param name="left"></param>
+ /// <param name="right"></param>
+ /// <returns>True if handles are equal, flase otherwise</returns>
+ public static bool operator ==(in UnsafeMemoryHandle<T> left, in UnsafeMemoryHandle<T> right) => left.Equals(right);
+ /// <summary>
+ /// Equality overload
+ /// </summary>
+ /// <param name="left"></param>
+ /// <param name="right"></param>
+ /// <returns>True if handles are equal, flase otherwise</returns>
+ public static bool operator !=(in UnsafeMemoryHandle<T> left, in UnsafeMemoryHandle<T> right) => !left.Equals(right);
+
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/VnString.cs b/lib/Utils/src/Memory/VnString.cs
new file mode 100644
index 0000000..7fa0c5a
--- /dev/null
+++ b/lib/Utils/src/Memory/VnString.cs
@@ -0,0 +1,497 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: VnString.cs
+*
+* VnString.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Text;
+using System.Buffers;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils.IO;
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.Memory
+{
+
+ /// <summary>
+ /// Provides an immutable character buffer stored on an unmanged heap. Contains handles to unmanged memory, and should be disposed
+ /// </summary>
+ [ComVisible(false)]
+ [ImmutableObject(true)]
+ public sealed class VnString : VnDisposeable, IEquatable<VnString>, IEquatable<string>, IEquatable<char[]>, IComparable<VnString>, IComparable<string>
+ {
+ private readonly MemoryHandle<char>? Handle;
+
+ private readonly SubSequence<char> _stringSequence;
+
+ /// <summary>
+ /// The number of unicode characters the current instance can reference
+ /// </summary>
+ public int Length => _stringSequence.Size;
+ /// <summary>
+ /// Gets a value indicating if the current instance is empty
+ /// </summary>
+ public bool IsEmpty => Length == 0;
+
+ private VnString(SubSequence<char> sequence)
+ {
+ _stringSequence = sequence;
+ }
+
+ private VnString(
+ MemoryHandle<char> handle,
+#if TARGET_64_BIT
+ ulong start,
+#else
+ int start,
+#endif
+ int length)
+ {
+ Handle = handle ?? throw new ArgumentNullException(nameof(handle));
+ //get sequence
+ _stringSequence = handle.GetSubSequence(start, length);
+ }
+
+ /// <summary>
+ /// Creates and empty <see cref="VnString"/>, not particularly usefull, just and empty instance.
+ /// </summary>
+ public VnString()
+ {
+ //Default string sequence is empty and does not hold any memory
+ }
+ /// <summary>
+ /// Creates a new <see cref="VnString"/> around a <see cref="ReadOnlySpan{T}"/> or a <see cref="string"/> of data
+ /// </summary>
+ /// <param name="data"><see cref="ReadOnlySpan{T}"/> of data to replicate</param>
+ /// <exception cref="OutOfMemoryException"></exception>
+ public VnString(ReadOnlySpan<char> data)
+ {
+ //Create new handle with enough size (heap)
+ Handle = Memory.Shared.Alloc<char>(data.Length);
+ //Copy
+ Memory.Copy(data, Handle, 0);
+ //Get subsequence over the whole copy of data
+ _stringSequence = Handle.GetSubSequence(0, data.Length);
+ }
+ /// <summary>
+ /// Allocates a temporary buffer to read data from the stream until the end of the stream is reached.
+ /// Decodes data from the user-specified encoding
+ /// </summary>
+ /// <param name="stream">Active stream of data to decode to a string</param>
+ /// <param name="encoding"><see cref="Encoding"/> to use for decoding</param>
+ /// <param name="bufferSize">The size of the buffer to allocate during copying</param>
+ /// <returns>The new <see cref="VnString"/> instance</returns>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ public static VnString FromStream(Stream stream, Encoding encoding, uint bufferSize)
+ {
+ //Make sure the stream is readable
+ if (!stream.CanRead)
+ {
+ throw new InvalidOperationException();
+ }
+ //See if the stream is a vn memory stream
+ if (stream is VnMemoryStream vnms)
+ {
+ //Get the number of characters
+ int numChars = encoding.GetCharCount(vnms.AsSpan());
+ //New handle
+ MemoryHandle<char> charBuffer = Memory.Shared.Alloc<char>(numChars);
+ try
+ {
+ //Write characters to character buffer
+ _ = encoding.GetChars(vnms.AsSpan(), charBuffer);
+ //Consume the new handle
+ return ConsumeHandle(charBuffer, 0, numChars);
+ }
+ catch
+ {
+ //If an error occured, dispose the buffer
+ charBuffer.Dispose();
+ throw;
+ }
+ }
+ //Need to read from the stream old school with buffers
+ else
+ {
+ //Create a new char bufer that will expand dyanmically
+ MemoryHandle<char> charBuffer = Memory.Shared.Alloc<char>(bufferSize);
+ //Allocate a binary buffer
+ MemoryHandle<byte> binBuffer = Memory.Shared.Alloc<byte>(bufferSize);
+ try
+ {
+ int length = 0;
+ //span ref to bin buffer
+ Span<byte> buffer = binBuffer;
+ //Run in checked context for overflows
+ checked
+ {
+ do
+ {
+ //read
+ int read = stream.Read(buffer);
+ //guard
+ if (read <= 0)
+ {
+ break;
+ }
+ //Slice into only the read data
+ ReadOnlySpan<byte> readbytes = buffer[..read];
+ //get num chars
+ int numChars = encoding.GetCharCount(readbytes);
+ //Guard for overflow
+ if (((ulong)(numChars + length)) >= int.MaxValue)
+ {
+ throw new OverflowException();
+ }
+ //Re-alloc buffer
+ charBuffer.ResizeIfSmaller(length + numChars);
+ //Decode and update position
+ _= encoding.GetChars(readbytes, charBuffer.Span.Slice(length, numChars));
+ //Update char count
+ length += numChars;
+ } while (true);
+ }
+ return ConsumeHandle(charBuffer, 0, length);
+ }
+ catch
+ {
+ //Free the memory allocated
+ charBuffer.Dispose();
+ //We still want the exception to be propagated!
+ throw;
+ }
+ finally
+ {
+ //Dispose the binary buffer
+ binBuffer.Dispose();
+ }
+ }
+ }
+ /// <summary>
+ /// Creates a new Vnstring from the <see cref="MemoryHandle{T}"/> buffer provided. This function "consumes"
+ /// a handle, meaning it now takes ownsership of the the memory it points to.
+ /// </summary>
+ /// <param name="handle">The <see cref="MemoryHandle{T}"/> to consume</param>
+ /// <param name="start">The offset from the begining of the buffer marking the begining of the string</param>
+ /// <param name="length">The number of characters this string points to</param>
+ /// <returns>The new <see cref="VnString"/></returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static VnString ConsumeHandle(
+ MemoryHandle<char> handle,
+
+#if TARGET_64_BIT
+ ulong start,
+#else
+ int start,
+#endif
+
+ int length)
+ {
+ if(length < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+ if((uint)length > handle.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+ return new VnString(handle, start, length);
+ }
+ /// <summary>
+ /// Asynchronously reads data from the specified stream and uses the specified encoding
+ /// to decode the binary data to a new <see cref="VnString"/> heap character buffer.
+ /// </summary>
+ /// <param name="stream">The stream to read data from</param>
+ /// <param name="encoding">The encoding to use while decoding data</param>
+ /// <param name="heap">The <see cref="IUnmangedHeap"/> to allocate buffers from</param>
+ /// <param name="bufferSize">The size of the buffer to allocate</param>
+ /// <returns>The new <see cref="VnString"/> containing the data</returns>
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="OutOfMemoryException"></exception>
+ public static async ValueTask<VnString> FromStreamAsync(Stream stream, Encoding encoding, IUnmangedHeap heap, int bufferSize)
+ {
+ _ = stream ?? throw new ArgumentNullException(nameof(stream));
+ _ = encoding ?? throw new ArgumentNullException(nameof(encoding));
+ _ = heap ?? throw new ArgumentNullException(nameof(heap));
+
+ //Make sure the stream is readable
+ if (!stream.CanRead)
+ {
+ throw new IOException("The input stream is not readable");
+ }
+ //See if the stream is a vn memory stream
+ if (stream is VnMemoryStream vnms)
+ {
+ //Get the number of characters
+ int numChars = encoding.GetCharCount(vnms.AsSpan());
+ //New handle
+ MemoryHandle<char> charBuffer = heap.Alloc<char>(numChars);
+ try
+ {
+ //Write characters to character buffer
+ _ = encoding.GetChars(vnms.AsSpan(), charBuffer);
+ //Consume the new handle
+ return ConsumeHandle(charBuffer, 0, numChars);
+ }
+ catch
+ {
+ //If an error occured, dispose the buffer
+ charBuffer.Dispose();
+ throw;
+ }
+ }
+ else
+ {
+ //Create a new char bufer starting with the buffer size
+ MemoryHandle<char> charBuffer = heap.Alloc<char>(bufferSize);
+ //Rent a temp binary buffer
+ IMemoryOwner<byte> binBuffer = heap.DirectAlloc<byte>(bufferSize);
+ try
+ {
+ int length = 0;
+ do
+ {
+ //read async
+ int read = await stream.ReadAsync(binBuffer.Memory);
+ //guard
+ if (read <= 0)
+ {
+ break;
+ }
+ //calculate the number of characters
+ int numChars = encoding.GetCharCount(binBuffer.Memory.Span[..read]);
+ //Guard for overflow
+ if (((ulong)(numChars + length)) >= int.MaxValue)
+ {
+ throw new OverflowException();
+ }
+ //Re-alloc buffer
+ charBuffer.ResizeIfSmaller(length + numChars);
+ //Decode and update position
+ _ = encoding.GetChars(binBuffer.Memory.Span[..read], charBuffer.Span.Slice(length, numChars));
+ //Update char count
+ length += numChars;
+ } while (true);
+ return ConsumeHandle(charBuffer, 0, length);
+ }
+ catch
+ {
+ //Free the memory allocated
+ charBuffer.Dispose();
+ //We still want the exception to be propagated!
+ throw;
+ }
+ finally
+ {
+ //Dispose the binary buffer
+ binBuffer.Dispose();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the value of the character at the specified index
+ /// </summary>
+ /// <param name="index">The index of the character to get</param>
+ /// <returns>The <see cref="char"/> at the specified index within the buffer</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public char CharAt(int index)
+ {
+ //Check
+ Check();
+ //Check bounds
+ return _stringSequence.Span[index];
+ }
+
+#pragma warning disable IDE0057 // Use range operator
+ /// <summary>
+ /// Creates a <see cref="VnString"/> that is a window within the current string,
+ /// the referrence points to the same memory as the first instnace.
+ /// </summary>
+ /// <param name="start">The index within the current string to begin the child string</param>
+ /// <param name="count">The number of characters (or length) of the child string</param>
+ /// <returns>The child <see cref="VnString"/></returns>
+ /// <remarks>
+ /// Making substrings will reference the parents's underlying <see cref="MemoryHandle{T}"/>
+ /// and all children will be set in a disposed state when the parent instance is disposed
+ /// </remarks>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public VnString Substring(int start, int count)
+ {
+ //Check
+ Check();
+ //Check bounds
+ if (start < 0 || (start + count) >= Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+ //get sub-sequence slice for the current string
+ SubSequence<char> sub = _stringSequence.Slice((uint)start, count);
+ //Create new string with offsets pointing to same internal referrence
+ return new VnString(sub);
+ }
+ /// <summary>
+ /// Creates a <see cref="VnString"/> that is a window within the current string,
+ /// the referrence points to the same memory as the first instnace.
+ /// </summary>
+ /// <param name="start">The index within the current string to begin the child string</param>
+ /// <returns>The child <see cref="VnString"/></returns>
+ /// <remarks>
+ /// Making substrings will reference the parents's underlying <see cref="MemoryHandle{T}"/>
+ /// and all children will be set in a disposed state when the parent instance is disposed
+ /// </remarks>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public VnString Substring(int start) => Substring(start, (Length - start));
+ public VnString this[Range range]
+ {
+ get
+ {
+ //get start
+ int start = range.Start.IsFromEnd ? Length - range.Start.Value : range.Start.Value;
+ //Get end
+ int end = range.End.IsFromEnd ? Length - range.End.Value : range.End.Value;
+ //Handle strings with no ending range
+ return (end >= start) ? Substring(start, (end - start)) : Substring(start);
+ }
+ }
+#pragma warning restore IDE0057 // Use range operator
+
+ /// <summary>
+ /// Gets a <see cref="ReadOnlySpan{T}"/> over the internal character buffer
+ /// </summary>
+ /// <returns></returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ReadOnlySpan<char> AsSpan()
+ {
+ //Check
+ Check();
+ return _stringSequence.Span;
+ }
+
+ /// <summary>
+ /// Gets a <see cref="string"/> copy of the internal buffer
+ /// </summary>
+ /// <returns><see cref="string"/> representation of internal data</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public override unsafe string ToString()
+ {
+ //Check
+ Check();
+ //Create a new
+ return AsSpan().ToString();
+ }
+ /// <summary>
+ /// Gets the value of the character at the specified index
+ /// </summary>
+ /// <param name="index">The index of the character to get</param>
+ /// <returns>The <see cref="char"/> at the specified index within the buffer</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public char this[int index] => CharAt(index);
+
+ //Casting to a vnstring should be explicit so the caller doesnt misuse memory managment
+ public static explicit operator ReadOnlySpan<char>(VnString? value) => Unsafe.IsNullRef(ref value) || value!.Disposed ? ReadOnlySpan<char>.Empty : value.AsSpan();
+ public static explicit operator VnString(string value) => new (value);
+ public static explicit operator VnString(ReadOnlySpan<char> value) => new (value);
+ public static explicit operator VnString(char[] value) => new (value);
+ ///<inheritdoc/>
+ public override bool Equals(object? obj)
+ {
+ if(obj == null)
+ {
+ return false;
+ }
+ return obj switch
+ {
+ VnString => Equals(obj as VnString), //Use operator overload
+ string => Equals(obj as string), //Use operator overload
+ char[] => Equals(obj as char[]), //Use operator overload
+ _ => false,
+ };
+ }
+ ///<inheritdoc/>
+ public bool Equals(VnString? other) => !ReferenceEquals(other, null) && Equals(other.AsSpan());
+ ///<inheritdoc/>
+ public bool Equals(VnString? other, StringComparison stringComparison) => !ReferenceEquals(other, null) && Equals(other.AsSpan(), stringComparison);
+ ///<inheritdoc/>
+ public bool Equals(string? other) => Equals(other.AsSpan());
+ ///<inheritdoc/>
+ public bool Equals(string? other, StringComparison stringComparison) => Equals(other.AsSpan(), stringComparison);
+ ///<inheritdoc/>
+ public bool Equals(char[]? other) => Equals(other.AsSpan());
+ ///<inheritdoc/>
+ public bool Equals(char[]? other, StringComparison stringComparison) => Equals(other.AsSpan(), stringComparison);
+ ///<inheritdoc/>
+ public bool Equals(ReadOnlySpan<char> other, StringComparison stringComparison = StringComparison.Ordinal) => Length == other.Length && AsSpan().Equals(other, stringComparison);
+ ///<inheritdoc/>
+ public bool Equals(SubSequence<char> other) => Length == other.Size && AsSpan().SequenceEqual(other.Span);
+ ///<inheritdoc/>
+ public int CompareTo(string? other) => AsSpan().CompareTo(other, StringComparison.Ordinal);
+ ///<inheritdoc/>
+ ///<exception cref="ArgumentNullException"></exception>
+ public int CompareTo(VnString? other)
+ {
+ _ = other ?? throw new ArgumentNullException(nameof(other));
+ return AsSpan().CompareTo(other.AsSpan(), StringComparison.Ordinal);
+ }
+
+ /// <summary>
+ /// Gets a hashcode for the underyling string by using the .NET <see cref="string.GetHashCode()"/>
+ /// method on the character representation of the data
+ /// </summary>
+ /// <returns></returns>
+ /// <remarks>
+ /// It is safe to compare hashcodes of <see cref="VnString"/> to the <see cref="string"/> class or
+ /// a character span etc
+ /// </remarks>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public override int GetHashCode() => string.GetHashCode(AsSpan());
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ //Dispose the handle if we own it (null if we do not have the parent handle)
+ Handle?.Dispose();
+ }
+
+ public static bool operator ==(VnString left, VnString right) => ReferenceEquals(left, null) ? ReferenceEquals(right, null) : left.Equals(right);
+
+ public static bool operator !=(VnString left, VnString right) => !(left == right);
+
+ public static bool operator <(VnString left, VnString right) => ReferenceEquals(left, null) ? !ReferenceEquals(right, null) : left.CompareTo(right) < 0;
+
+ public static bool operator <=(VnString left, VnString right) => ReferenceEquals(left, null) || left.CompareTo(right) <= 0;
+
+ public static bool operator >(VnString left, VnString right) => !ReferenceEquals(left, null) && left.CompareTo(right) > 0;
+
+ public static bool operator >=(VnString left, VnString right) => ReferenceEquals(left, null) ? ReferenceEquals(right, null) : left.CompareTo(right) >= 0;
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/VnTable.cs b/lib/Utils/src/Memory/VnTable.cs
new file mode 100644
index 0000000..1d5c0a6
--- /dev/null
+++ b/lib/Utils/src/Memory/VnTable.cs
@@ -0,0 +1,213 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: VnTable.cs
+*
+* VnTable.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// Provides a Row-Major ordered table for use of storing value-types in umnaged heap memory
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public sealed class VnTable<T> : VnDisposeable, IIndexable<uint, T> where T : unmanaged
+ {
+ private readonly MemoryHandle<T>? BufferHandle;
+ /// <summary>
+ /// A value that indicates if the table does not contain any values
+ /// </summary>
+ public bool Empty { get; }
+ /// <summary>
+ /// The number of rows in the table
+ /// </summary>
+ public int Rows { get; }
+ /// <summary>
+ /// The nuber of columns in the table
+ /// </summary>
+ public int Cols { get; }
+ /// <summary>
+ /// Creates a new 2 dimensional table in unmanaged heap memory, using the <see cref="Memory.Shared"/> heap.
+ /// User should dispose of the table when no longer in use
+ /// </summary>
+ /// <param name="rows">Number of rows in the table</param>
+ /// <param name="cols">Number of columns in the table</param>
+ public VnTable(int rows, int cols) : this(Memory.Shared, rows, cols) { }
+ /// <summary>
+ /// Creates a new 2 dimensional table in unmanaged heap memory, using the specified heap.
+ /// User should dispose of the table when no longer in use
+ /// </summary>
+ /// <param name="heap"><see cref="PrivateHeap"/> to allocate table memory from</param>
+ /// <param name="rows">Number of rows in the table</param>
+ /// <param name="cols">Number of columns in the table</param>
+ public VnTable(IUnmangedHeap heap, int rows, int cols)
+ {
+ if (rows < 0 || cols < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(rows), "Row and coulmn number must be 0 or larger");
+ }
+ //empty table
+ if (rows == 0 && cols == 0)
+ {
+ Empty = true;
+ return;
+ }
+
+ _ = heap ?? throw new ArgumentNullException(nameof(heap));
+
+ this.Rows = rows;
+ this.Cols = cols;
+
+ long tableSize = Math.BigMul(rows, cols);
+
+ //Alloc a buffer with zero memory enabled, with Rows * Cols number of elements
+ BufferHandle = heap.Alloc<T>(tableSize, true);
+ }
+ /// <summary>
+ /// Gets the value of an item in the table at the given indexes
+ /// </summary>
+ /// <param name="row">Row address of the item</param>
+ /// <param name="col">Column address of item</param>
+ /// <returns>The value of the item</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public T Get(int row, int col)
+ {
+ Check();
+ if (this.Empty)
+ {
+ throw new InvalidOperationException("Table is empty");
+ }
+ if (row < 0 || col < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(row), "Row or column address less than 0");
+ }
+ if (row > this.Rows)
+ {
+ throw new ArgumentOutOfRangeException(nameof(row), "Row out of range of current table");
+ }
+ if (col > this.Cols)
+ {
+ throw new ArgumentOutOfRangeException(nameof(col), "Column address out of range of current table");
+ }
+ //Calculate the address in memory for the item
+ //Calc row offset
+ long address = Cols * row;
+ //Calc column offset
+ address += col;
+ unsafe
+ {
+ //Get the value item
+ return *(BufferHandle!.GetOffset(address));
+ }
+ }
+ /// <summary>
+ /// Sets the value of an item in the table at the given address
+ /// </summary>
+ /// <param name="item">Value of item to store</param>
+ /// <param name="row">Row address of the item</param>
+ /// <param name="col">Column address of item</param>
+ /// <returns>The value of the item</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public void Set(int row, int col, T item)
+ {
+ Check();
+ if (this.Empty)
+ {
+ throw new InvalidOperationException("Table is empty");
+ }
+ if (row < 0 || col < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(row), "Row or column address less than 0");
+ }
+ if (row > this.Rows)
+ {
+ throw new ArgumentOutOfRangeException(nameof(row), "Row out of range of current table");
+ }
+ if (col > this.Cols)
+ {
+ throw new ArgumentOutOfRangeException(nameof(col), "Column address out of range of current table");
+ }
+ //Calculate the address in memory for the item
+ //Calc row offset
+ long address = Cols * row;
+ //Calc column offset
+ address += col;
+ //Set the value item
+ unsafe
+ {
+ *BufferHandle!.GetOffset(address) = item;
+ }
+ }
+ /// <summary>
+ /// Equivalent to <see cref="VnTable{T}.Get(int, int)"/> and <see cref="VnTable{T}.Set(int, int, T)"/>
+ /// </summary>
+ /// <param name="row">Row address of item</param>
+ /// <param name="col">Column address of item</param>
+ /// <returns>The value of the item</returns>
+ public T this[int row, int col]
+ {
+ get => Get(row, col);
+ set => Set(row, col, value);
+ }
+ /// <summary>
+ /// Allows for direct addressing in the table.
+ /// </summary>
+ /// <param name="index"></param>
+ /// <returns></returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public unsafe T this[uint index]
+ {
+ get
+ {
+ Check();
+ return !Empty ? *(BufferHandle!.GetOffset(index)) : throw new InvalidOperationException("Cannot index an empty table");
+ }
+
+ set
+ {
+ Check();
+ if (Empty)
+ {
+ throw new InvalidOperationException("Cannot index an empty table");
+ }
+ *(BufferHandle!.GetOffset(index)) = value;
+ }
+ }
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ if (!this.Empty)
+ {
+ //Dispose the buffer
+ BufferHandle!.Dispose();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Memory/VnTempBuffer.cs b/lib/Utils/src/Memory/VnTempBuffer.cs
new file mode 100644
index 0000000..7726fe1
--- /dev/null
+++ b/lib/Utils/src/Memory/VnTempBuffer.cs
@@ -0,0 +1,207 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: VnTempBuffer.cs
+*
+* VnTempBuffer.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils.Extensions;
+using System.Security.Cryptography;
+
+namespace VNLib.Utils.Memory
+{
+ /// <summary>
+ /// A disposable temporary buffer from shared ArrayPool
+ /// </summary>
+ /// <typeparam name="T">Type of buffer to create</typeparam>
+ public sealed class VnTempBuffer<T> : VnDisposeable, IIndexable<int, T>, IMemoryHandle<T>
+ {
+ private readonly ArrayPool<T> Pool;
+
+ /// <summary>
+ /// Referrence to internal buffer
+ /// </summary>
+ public T[] Buffer { get; private set; }
+ /// <summary>
+ /// Inital/desired size of internal buffer
+ /// </summary>
+ public int InitSize { get; }
+
+ /// <summary>
+ /// Actual length of internal buffer
+ /// </summary>
+ public ulong Length => (ulong)Buffer.LongLength;
+
+ /// <summary>
+ /// Actual length of internal buffer
+ /// </summary>
+ public int IntLength => Buffer.Length;
+
+ ///<inheritdoc/>
+ ///<exception cref="ObjectDisposedException"></exception>
+ public Span<T> Span
+ {
+ get
+ {
+ Check();
+ return new Span<T>(Buffer, 0, InitSize);
+ }
+ }
+
+ /// <summary>
+ /// Allocates a new <see cref="VnTempBuffer{BufType}"/> with a new buffer from shared array-pool
+ /// </summary>
+ /// <param name="minSize">Minimum size of the buffer</param>
+ /// <param name="zero">Set the zero memory flag on close</param>
+ public VnTempBuffer(int minSize, bool zero = false) :this(ArrayPool<T>.Shared, minSize, zero)
+ {}
+ /// <summary>
+ /// Allocates a new <see cref="VnTempBuffer{BufType}"/> with a new buffer from specified array-pool
+ /// </summary>
+ /// <param name="pool">The <see cref="ArrayPool{T}"/> to allocate from and return to</param>
+ /// <param name="minSize">Minimum size of the buffer</param>
+ /// <param name="zero">Set the zero memory flag on close</param>
+ public VnTempBuffer(ArrayPool<T> pool, int minSize, bool zero = false)
+ {
+ Pool = pool;
+ Buffer = pool.Rent(minSize, zero);
+ InitSize = minSize;
+ }
+ /// <summary>
+ /// Gets an offset wrapper around the current buffer
+ /// </summary>
+ /// <param name="offset">Offset from begining of current buffer</param>
+ /// <param name="count">Number of <typeparamref name="T"/> from offset</param>
+ /// <returns>An <see cref="ArraySegment{BufType}"/> wrapper around the current buffer containing the offset</returns>
+ public ArraySegment<T> GetOffsetWrapper(int offset, int count)
+ {
+ Check();
+ //Let arraysegment throw exceptions for checks
+ return new ArraySegment<T>(Buffer, offset, count);
+ }
+ ///<inheritdoc/>
+ public T this[int index]
+ {
+ get
+ {
+ Check();
+ return Buffer[index];
+ }
+ set
+ {
+ Check();
+ Buffer[index] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets a memory structure around the internal buffer
+ /// </summary>
+ /// <returns>A memory structure over the buffer</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public Memory<T> AsMemory()
+ {
+ Check();
+ return new Memory<T>(Buffer, 0, InitSize);
+ }
+ /// <summary>
+ /// Gets a memory structure around the internal buffer
+ /// </summary>
+ /// <param name="count">The number of elements included in the result</param>
+ /// <param name="start">A value specifying the begining index of the buffer to include</param>
+ /// <returns>A memory structure over the buffer</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public Memory<T> AsMemory(int start, int count)
+ {
+ Check();
+ return new Memory<T>(Buffer, start, count);
+ }
+ /// <summary>
+ /// Gets a memory structure around the internal buffer
+ /// </summary>
+ /// <param name="count">The number of elements included in the result</param>
+ /// <returns>A memory structure over the buffer</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public Memory<T> AsMemory(int count)
+ {
+ Check();
+ return new Memory<T>(Buffer, 0, count);
+ }
+
+ /*
+ * Allow implict casts to span/arrayseg/memory
+ */
+ public static implicit operator Memory<T>(VnTempBuffer<T> buf) => buf == null ? Memory<T>.Empty : buf.ToMemory();
+ public static implicit operator Span<T>(VnTempBuffer<T> buf) => buf == null ? Span<T>.Empty : buf.ToSpan();
+ public static implicit operator ArraySegment<T>(VnTempBuffer<T> buf) => buf == null ? ArraySegment<T>.Empty : buf.ToArraySegment();
+
+ public Memory<T> ToMemory() => Disposed ? Memory<T>.Empty : Buffer.AsMemory(0, InitSize);
+ public Span<T> ToSpan() => Disposed ? Span<T>.Empty : Buffer.AsSpan(0, InitSize);
+ public ArraySegment<T> ToArraySegment() => Disposed ? ArraySegment<T>.Empty : new(Buffer, 0, InitSize);
+
+ /// <summary>
+ /// Returns buffer to shared array-pool
+ /// </summary>
+ protected override void Free()
+ {
+ //Return the buffer to the array pool
+ Pool.Return(Buffer);
+
+ //Set buffer to null,
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ Buffer = null;
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+ }
+
+ unsafe MemoryHandle IPinnable.Pin(int elementIndex)
+ {
+ //Guard
+ if (elementIndex < 0 || elementIndex >= IntLength)
+ {
+ throw new ArgumentOutOfRangeException(nameof(elementIndex));
+ }
+
+ //Pin the array
+ GCHandle arrHandle = GCHandle.Alloc(Buffer, GCHandleType.Pinned);
+ //Get array base address
+ void* basePtr = (void*)arrHandle.AddrOfPinnedObject();
+ //Get element offset
+ void* indexOffet = Unsafe.Add<T>(basePtr, elementIndex);
+ return new(indexOffet, arrHandle, this);
+ }
+
+ void IPinnable.Unpin()
+ {
+ //Gchandle will manage the unpin
+ }
+
+ ~VnTempBuffer() => Free();
+
+
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Native/SafeLibraryHandle.cs b/lib/Utils/src/Native/SafeLibraryHandle.cs
new file mode 100644
index 0000000..4772bd4
--- /dev/null
+++ b/lib/Utils/src/Native/SafeLibraryHandle.cs
@@ -0,0 +1,220 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: SafeLibraryHandle.cs
+*
+* SafeLibraryHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Diagnostics.CodeAnalysis;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Utils.Native
+{
+ /// <summary>
+ /// Represents a safe handle to a native library loaded to the current process
+ /// </summary>
+ public sealed class SafeLibraryHandle : SafeHandle
+ {
+ ///<inheritdoc/>
+ public override bool IsInvalid => handle == IntPtr.Zero;
+
+ private SafeLibraryHandle(IntPtr libHandle) : base(IntPtr.Zero, true)
+ {
+ //Init handle
+ SetHandle(libHandle);
+ }
+
+ /// <summary>
+ /// Finds and loads the specified native libary into the current process by its name at runtime
+ /// </summary>
+ /// <param name="libPath">The path (or name of libary) to search for</param>
+ /// <param name="searchPath">
+ /// The <see cref="DllImportSearchPath"/> used to search for libaries
+ /// within the current filesystem
+ /// </param>
+ /// <returns>The loaded <see cref="SafeLibraryHandle"/></returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="DllNotFoundException"></exception>
+ public static SafeLibraryHandle LoadLibrary(string libPath, DllImportSearchPath searchPath = DllImportSearchPath.ApplicationDirectory)
+ {
+ _ = libPath ?? throw new ArgumentNullException(nameof(libPath));
+ //See if the path includes a file extension
+ return TryLoadLibrary(libPath, searchPath, out SafeLibraryHandle? lib)
+ ? lib
+ : throw new DllNotFoundException($"The library {libPath} or one of its dependencies could not be found");
+ }
+
+ /// <summary>
+ /// Attempts to load the specified native libary into the current process by its name at runtime
+ /// </summary>
+ ///<param name="libPath">The path (or name of libary) to search for</param>
+ /// <param name="searchPath">
+ /// The <see cref="DllImportSearchPath"/> used to search for libaries
+ /// within the current filesystem
+ /// </param>
+ /// <param name="lib">The handle to the libary if successfully loaded</param>
+ /// <returns>True if the libary was found and loaded into the current process</returns>
+ public static bool TryLoadLibrary(string libPath, DllImportSearchPath searchPath, [NotNullWhen(true)] out SafeLibraryHandle? lib)
+ {
+ lib = null;
+ //Allow full rooted paths
+ if (Path.IsPathRooted(libPath))
+ {
+ //Attempt a native load
+ if (NativeLibrary.TryLoad(libPath, out IntPtr libHandle))
+ {
+ lib = new(libHandle);
+ return true;
+ }
+ return false;
+ }
+ //Check application directory first (including subdirectories)
+ if ((searchPath & DllImportSearchPath.ApplicationDirectory) > 0)
+ {
+ //get the current directory
+ string libDir = Directory.GetCurrentDirectory();
+ if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib))
+ {
+ return true;
+ }
+ }
+ //See if search in the calling assembly directory
+ if ((searchPath & DllImportSearchPath.AssemblyDirectory) > 0)
+ {
+ //Get the calling assmblies directory
+ string libDir = Assembly.GetCallingAssembly().Location;
+ Debug.WriteLine("Native library searching for calling assembly location:{0} ", libDir);
+ if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib))
+ {
+ return true;
+ }
+ }
+ //Search system32 dir
+ if ((searchPath & DllImportSearchPath.System32) > 0)
+ {
+ //Get the system directory
+ string libDir = Environment.GetFolderPath(Environment.SpecialFolder.SystemX86);
+ if (TryLoadLibraryInternal(libDir, libPath, SearchOption.TopDirectoryOnly, out lib))
+ {
+ return true;
+ }
+ }
+ //Attempt a native load
+ {
+ if (NativeLibrary.TryLoad(libPath, out IntPtr libHandle))
+ {
+ lib = new(libHandle);
+ return true;
+ }
+ return false;
+ }
+ }
+
+ private static bool TryLoadLibraryInternal(string libDir, string libPath, SearchOption dirSearchOptions, [NotNullWhen(true)] out SafeLibraryHandle? libary)
+ {
+ //Try to find the libary file
+ string? libFile = GetLibraryFile(libDir, libPath, dirSearchOptions);
+ //Load libary
+ if (libFile != null && NativeLibrary.TryLoad(libFile, out IntPtr libHandle))
+ {
+ libary = new SafeLibraryHandle(libHandle);
+ return true;
+ }
+ libary = null;
+ return false;
+ }
+ private static string? GetLibraryFile(string dirPath, string libPath, SearchOption search)
+ {
+ //slice the lib to its file name
+ libPath = Path.GetFileName(libPath);
+ libPath = Path.ChangeExtension(libPath, OperatingSystem.IsWindows() ? ".dll" : ".so");
+ //Select the first file that matches the name
+ return Directory.EnumerateFiles(dirPath, libPath, search).FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Loads a native method from the library of the specified name and managed delegate
+ /// </summary>
+ /// <typeparam name="T">The native method delegate type</typeparam>
+ /// <param name="methodName">The name of the native method</param>
+ /// <returns>A wapper handle around the native method delegate</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ObjectDisposedException">If the handle is closed or invalid</exception>
+ /// <exception cref="EntryPointNotFoundException">When the specified entrypoint could not be found</exception>
+ public SafeMethodHandle<T> GetMethod<T>(string methodName) where T : Delegate
+ {
+ //Increment handle count before obtaining a method
+ bool success = false;
+ DangerousAddRef(ref success);
+ if (!success)
+ {
+ throw new ObjectDisposedException("The libary has been released!");
+ }
+ try
+ {
+ //Get the method pointer
+ IntPtr nativeMethod = NativeLibrary.GetExport(handle, methodName);
+ //Get the delegate for the function pointer
+ T method = Marshal.GetDelegateForFunctionPointer<T>(nativeMethod);
+ return new(this, method);
+ }
+ catch
+ {
+ DangerousRelease();
+ throw;
+ }
+ }
+ /// <summary>
+ /// Gets an delegate wrapper for the specified method without tracking its referrence.
+ /// The caller must manage the <see cref="SafeLibraryHandle"/> referrence count in order
+ /// to not leak resources or cause process corruption
+ /// </summary>
+ /// <typeparam name="T">The native method delegate type</typeparam>
+ /// <param name="methodName">The name of the native method</param>
+ /// <returns>A the delegate wrapper on the native method</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ObjectDisposedException">If the handle is closed or invalid</exception>
+ /// <exception cref="EntryPointNotFoundException">When the specified entrypoint could not be found</exception>
+ public T DangerousGetMethod<T>(string methodName) where T : Delegate
+ {
+ this.ThrowIfClosed();
+ //Get the method pointer
+ IntPtr nativeMethod = NativeLibrary.GetExport(handle, methodName);
+ //Get the delegate for the function pointer
+ return Marshal.GetDelegateForFunctionPointer<T>(nativeMethod);
+ }
+
+ ///<inheritdoc/>
+ protected override bool ReleaseHandle()
+ {
+ //Free the library and set the handle as invalid
+ NativeLibrary.Free(handle);
+ SetHandleAsInvalid();
+ return true;
+ }
+ }
+}
diff --git a/lib/Utils/src/Native/SafeMethodHandle.cs b/lib/Utils/src/Native/SafeMethodHandle.cs
new file mode 100644
index 0000000..3ba0879
--- /dev/null
+++ b/lib/Utils/src/Native/SafeMethodHandle.cs
@@ -0,0 +1,61 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: SafeMethodHandle.cs
+*
+* SafeMethodHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using VNLib.Utils.Resources;
+
+namespace VNLib.Utils.Native
+{
+ /// <summary>
+ /// Represents a handle to a <see cref="SafeLibraryHandle"/>'s
+ /// native method
+ /// </summary>
+ /// <typeparam name="T">The native method deelgate type</typeparam>
+ public class SafeMethodHandle<T> : OpenHandle where T : Delegate
+ {
+ private T? _method;
+ private readonly SafeLibraryHandle Library;
+
+ internal SafeMethodHandle(SafeLibraryHandle lib, T method)
+ {
+ Library = lib;
+ _method = method;
+ }
+
+ /// <summary>
+ /// A delegate to the native method
+ /// </summary>
+ public T? Method => _method;
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ //Release the method
+ _method = default;
+ //Decrement lib handle count
+ Library.DangerousRelease();
+ }
+ }
+}
diff --git a/lib/Utils/src/NativeLibraryException.cs b/lib/Utils/src/NativeLibraryException.cs
new file mode 100644
index 0000000..5c66852
--- /dev/null
+++ b/lib/Utils/src/NativeLibraryException.cs
@@ -0,0 +1,89 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: NativeLibraryException.cs
+*
+* NativeLibraryException.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils
+{
+ /// <summary>
+ /// Raised when an internal buffer was not propery sized for the opreation
+ /// </summary>
+ public class InternalBufferTooSmallException : OutOfMemoryException
+ {
+ public InternalBufferTooSmallException(string message) : base(message)
+ {}
+
+ public InternalBufferTooSmallException(string message, Exception innerException) : base(message, innerException)
+ {}
+
+ public InternalBufferTooSmallException()
+ {}
+ }
+
+ /// <summary>
+ /// A base class for all native library related exceptions
+ /// </summary>
+ public class NativeLibraryException : SystemException
+ {
+ public NativeLibraryException(string message) : base(message)
+ {}
+
+ public NativeLibraryException(string message, Exception innerException) : base(message, innerException)
+ {}
+
+ public NativeLibraryException()
+ {}
+ }
+
+ /// <summary>
+ /// Base exception class for native memory related exceptions
+ /// </summary>
+ public class NativeMemoryException : NativeLibraryException
+ {
+ public NativeMemoryException(string message) : base(message)
+ {}
+
+ public NativeMemoryException(string message, Exception innerException) : base(message, innerException)
+ {}
+
+ public NativeMemoryException()
+ {}
+ }
+
+ /// <summary>
+ /// Raised when a memory allocation or resize failed because there is
+ /// no more memory available
+ /// </summary>
+ public class NativeMemoryOutOfMemoryException : OutOfMemoryException
+ {
+ public NativeMemoryOutOfMemoryException(string message) : base(message)
+ {}
+
+ public NativeMemoryOutOfMemoryException(string message, Exception innerException) : base(message, innerException)
+ {}
+
+ public NativeMemoryOutOfMemoryException()
+ {}
+ }
+}
diff --git a/lib/Utils/src/Resources/BackedResourceBase.cs b/lib/Utils/src/Resources/BackedResourceBase.cs
new file mode 100644
index 0000000..0a2e1e3
--- /dev/null
+++ b/lib/Utils/src/Resources/BackedResourceBase.cs
@@ -0,0 +1,79 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: BackedResourceBase.cs
+*
+* BackedResourceBase.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Text.Json;
+using System.Runtime.CompilerServices;
+
+namespace VNLib.Utils.Resources
+{
+ /// <summary>
+ /// A base class for a resource that is backed by an external data store.
+ /// Implements the <see cref="IResource"/> interfaceS
+ /// </summary>
+ public abstract class BackedResourceBase : IResource
+ {
+ ///<inheritdoc/>
+ public bool IsReleased { get; protected set; }
+
+ /// <summary>
+ /// Optional <see cref="JsonSerializerOptions"/> to be used when serializing
+ /// the resource
+ /// </summary>
+ internal protected virtual JsonSerializerOptions? JSO { get; }
+
+ /// <summary>
+ /// A value indicating whether the instance should be deleted when released
+ /// </summary>
+ protected bool Deleted { get; set; }
+ /// <summary>
+ /// A value indicating whether the instance should be updated when released
+ /// </summary>
+ protected bool Modified { get; set; }
+
+ /// <summary>
+ /// Checks if the resouce has been disposed and raises an exception if it is
+ /// </summary>
+ /// <exception cref="ObjectDisposedException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected void Check()
+ {
+ if (IsReleased)
+ {
+ throw new ObjectDisposedException("The resource has been disposed");
+ }
+ }
+
+ /// <summary>
+ /// Returns the JSON serializable resource to be updated during an update
+ /// </summary>
+ /// <returns>The resource to update</returns>
+ protected abstract object GetResource();
+
+ /// <summary>
+ /// Marks the resource for deletion from backing store during closing events
+ /// </summary>
+ public virtual void Delete() => Deleted = true;
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Resources/CallbackOpenHandle.cs b/lib/Utils/src/Resources/CallbackOpenHandle.cs
new file mode 100644
index 0000000..625bd45
--- /dev/null
+++ b/lib/Utils/src/Resources/CallbackOpenHandle.cs
@@ -0,0 +1,44 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: CallbackOpenHandle.cs
+*
+* CallbackOpenHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Resources
+{
+ /// <summary>
+ /// A concrete <see cref="OpenHandle"/> for a defered operation or a resource that should be released or unwound
+ /// when the instance lifetime has ended.
+ /// </summary>
+ public sealed class CallbackOpenHandle : OpenHandle
+ {
+ private readonly Action ReleaseFunc;
+ /// <summary>
+ /// Creates a new generic <see cref="OpenHandle"/> with the specified release callback method
+ /// </summary>
+ /// <param name="release">The callback function to invoke when the <see cref="OpenHandle"/> is disposed</param>
+ public CallbackOpenHandle(Action release) => ReleaseFunc = release;
+ ///<inheritdoc/>
+ protected override void Free() => ReleaseFunc();
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Resources/ExclusiveResourceHandle.cs b/lib/Utils/src/Resources/ExclusiveResourceHandle.cs
new file mode 100644
index 0000000..173bdd1
--- /dev/null
+++ b/lib/Utils/src/Resources/ExclusiveResourceHandle.cs
@@ -0,0 +1,81 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ExclusiveResourceHandle.cs
+*
+* ExclusiveResourceHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Resources
+{
+ /// <summary>
+ /// While in scope, holds an exclusive lock on the specified object that implements the <see cref="IExclusiveResource"/> interface
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public class ExclusiveResourceHandle<T> : OpenResourceHandle<T> where T : IExclusiveResource
+ {
+ private readonly Action Release;
+ private readonly Lazy<T> LazyVal;
+
+ /// <summary>
+ /// <inheritdoc/>
+ /// <br></br>
+ /// <br></br>
+ /// This value is lazy inialized and will invoke the factory function on first access.
+ /// Accessing this variable is thread safe while the handle is in scope
+ /// <br></br>
+ /// <br></br>
+ /// Exceptions will be propagated during initialziation
+ /// </summary>
+ public override T Resource => LazyVal.Value;
+
+ /// <summary>
+ /// Creates a new <see cref="ExclusiveResourceHandle{TResource}"/> wrapping the
+ /// <see cref="IExclusiveResource"/> object to manage its lifecycle and reuse
+ /// </summary>
+ /// <param name="factory">Factory function that will generate the value when used</param>
+ /// <param name="release">Callback function that will be invoked after object gets disposed</param>
+ internal ExclusiveResourceHandle(Func<T> factory, Action release)
+ {
+ //Store the release action
+ Release = release;
+ //Store the new lazy val from the factory function (enabled thread safey)
+ LazyVal = new(factory, System.Threading.LazyThreadSafetyMode.ExecutionAndPublication);
+ }
+
+ protected override void Free()
+ {
+ try
+ {
+ //Dispose the value if it was created, otherwise do not create it
+ if (LazyVal.IsValueCreated)
+ {
+ Resource?.Release();
+ }
+ }
+ finally
+ {
+ //Always invoke the release callback
+ Release();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Resources/IExclusiveResource.cs b/lib/Utils/src/Resources/IExclusiveResource.cs
new file mode 100644
index 0000000..43ec607
--- /dev/null
+++ b/lib/Utils/src/Resources/IExclusiveResource.cs
@@ -0,0 +1,39 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IExclusiveResource.cs
+*
+* IExclusiveResource.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Utils.Resources
+{
+
+ /// <summary>
+ /// An object, that when used in a mulithreading context, guaruntees that the caller has exclusive
+ /// access to the instance and relinquishes exclusive access when Release() is called;
+ /// </summary>
+ public interface IExclusiveResource : IResource
+ {
+ /// <summary>
+ /// Releases the resource from use. Called when a <see cref="ExclusiveResourceHandle{T}"/> is disposed
+ /// </summary>
+ void Release();
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Resources/IResource.cs b/lib/Utils/src/Resources/IResource.cs
new file mode 100644
index 0000000..345e284
--- /dev/null
+++ b/lib/Utils/src/Resources/IResource.cs
@@ -0,0 +1,38 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: IResource.cs
+*
+* IResource.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Utils.Resources
+{
+ /// <summary>
+ /// The base interface for any resource that may be backed by an external
+ /// data store, and has a finite lifetime, usually accessed within a handle
+ /// </summary>
+ public interface IResource
+ {
+ /// <summary>
+ /// Gets a value indicating if the resource has been released
+ /// </summary>
+ bool IsReleased { get; }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Resources/OpenHandle.cs b/lib/Utils/src/Resources/OpenHandle.cs
new file mode 100644
index 0000000..6133a65
--- /dev/null
+++ b/lib/Utils/src/Resources/OpenHandle.cs
@@ -0,0 +1,38 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: OpenHandle.cs
+*
+* OpenHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Utils.Resources
+{
+ /// <summary>
+ /// Represents a base class for an open resource or operation that is valid while being held,
+ /// and is released or unwound when disposed.
+ /// </summary>
+ /// <remarks>
+ /// The <see cref="OpenHandle"/> pattern, may throw exceptions when disposed as deferred
+ /// release actions are completed
+ /// </remarks>
+ public abstract class OpenHandle : VnDisposeable
+ {
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Resources/OpenResourceHandle.cs b/lib/Utils/src/Resources/OpenResourceHandle.cs
new file mode 100644
index 0000000..d9f9fd2
--- /dev/null
+++ b/lib/Utils/src/Resources/OpenResourceHandle.cs
@@ -0,0 +1,44 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: OpenResourceHandle.cs
+*
+* OpenResourceHandle.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Utils.Resources
+{
+ /// <summary>
+ /// <para>
+ /// An abstract base class for an <see cref="OpenHandle"/> that holds a specific resource and manages its lifetime.
+ /// </para>
+ /// </summary>
+ /// <inheritdoc/>
+ /// <typeparam name="TResource">The resource type</typeparam>
+ public abstract class OpenResourceHandle<TResource> : OpenHandle
+ {
+ /// <summary>
+ /// The resource held by the open handle
+ /// </summary>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public abstract TResource Resource { get; }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Resources/ResourceDeleteFailedException.cs b/lib/Utils/src/Resources/ResourceDeleteFailedException.cs
new file mode 100644
index 0000000..8e796b5
--- /dev/null
+++ b/lib/Utils/src/Resources/ResourceDeleteFailedException.cs
@@ -0,0 +1,40 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ResourceDeleteFailedException.cs
+*
+* ResourceDeleteFailedException.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Runtime.Serialization;
+
+namespace VNLib.Utils.Resources
+{
+ /// <summary>
+ /// Raised when a resource delete has failed
+ /// </summary>
+ public class ResourceDeleteFailedException : Exception
+ {
+ public ResourceDeleteFailedException() { }
+ public ResourceDeleteFailedException(string message) : base(message) { }
+ public ResourceDeleteFailedException(string message, Exception innerException) : base(message, innerException) { }
+ protected ResourceDeleteFailedException(SerializationInfo info, StreamingContext context) : base(info, context) { }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Resources/ResourceUpdateFailedException.cs b/lib/Utils/src/Resources/ResourceUpdateFailedException.cs
new file mode 100644
index 0000000..b4b2b3a
--- /dev/null
+++ b/lib/Utils/src/Resources/ResourceUpdateFailedException.cs
@@ -0,0 +1,40 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: ResourceUpdateFailedException.cs
+*
+* ResourceUpdateFailedException.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Runtime.Serialization;
+
+namespace VNLib.Utils.Resources
+{
+ /// <summary>
+ /// Raised when a resource update has failed
+ /// </summary>
+ public class ResourceUpdateFailedException : Exception
+ {
+ public ResourceUpdateFailedException() { }
+ public ResourceUpdateFailedException(string message) : base(message) { }
+ public ResourceUpdateFailedException(string message, Exception innerException) : base(message, innerException) { }
+ protected ResourceUpdateFailedException(SerializationInfo info, StreamingContext context) : base(info, context) { }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/Resources/UpdatableResource.cs b/lib/Utils/src/Resources/UpdatableResource.cs
new file mode 100644
index 0000000..16f26f2
--- /dev/null
+++ b/lib/Utils/src/Resources/UpdatableResource.cs
@@ -0,0 +1,113 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: UpdatableResource.cs
+*
+* UpdatableResource.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+
+using VNLib.Utils.IO;
+
+namespace VNLib.Utils.Resources
+{
+ /// <summary>
+ /// A callback delegate used for updating a <see cref="UpdatableResource"/>
+ /// </summary>
+ /// <param name="source">The <see cref="UpdatableResource"/> to be updated</param>
+ /// <param name="data">The serialized data to be stored/updated</param>
+ /// <exception cref="ResourceUpdateFailedException"></exception>
+ public delegate void UpdateCallback(object source, Stream data);
+ /// <summary>
+ /// A callback delegate invoked when a <see cref="UpdatableResource"/> delete is requested
+ /// </summary>
+ /// <param name="source">The <see cref="UpdatableResource"/> to be deleted</param>
+ /// <exception cref="ResourceDeleteFailedException"></exception>
+ public delegate void DeleteCallback(object source);
+
+ /// <summary>
+ /// Implemented by a resource that is backed by an external data store, that when modified or deleted will
+ /// be reflected to the backing store.
+ /// </summary>
+ public abstract class UpdatableResource : BackedResourceBase, IExclusiveResource
+ {
+ /// <summary>
+ /// The update callback method to invoke during a release operation
+ /// when the resource is updated.
+ /// </summary>
+ protected abstract UpdateCallback UpdateCb { get; }
+ /// <summary>
+ /// The callback method to invoke during a realease operation
+ /// when the resource should be deleted
+ /// </summary>
+ protected abstract DeleteCallback DeleteCb { get; }
+
+ /// <summary>
+ /// <inheritdoc/>
+ /// </summary>
+ /// <exception cref="InvalidOperationException"></exception>
+ /// <exception cref="ResourceDeleteFailedException"></exception>
+ /// <exception cref="ResourceUpdateFailedException"></exception>
+ public virtual void Release()
+ {
+ //If resource has already been realeased, return
+ if (IsReleased)
+ {
+ return;
+ }
+ //If deleted flag is set, invoke the delete callback
+ if (Deleted)
+ {
+ DeleteCb(this);
+ }
+ //If the state has been modifed, flush changes to the store
+ else if (Modified)
+ {
+ FlushPendingChanges();
+ }
+ //Set the released value
+ IsReleased = true;
+ }
+
+ /// <summary>
+ /// Writes the current state of the the resource to the backing store
+ /// immediatly by invoking the specified callback.
+ /// <br></br>
+ /// <br></br>
+ /// Only call this method if your store supports multiple state updates
+ /// </summary>
+ protected virtual void FlushPendingChanges()
+ {
+ //Get the resource
+ object resource = GetResource();
+ //Open a memory stream to store data in
+ using VnMemoryStream data = new();
+ //Serialize and write to stream
+ VnEncoding.JSONSerializeToBinary(resource, data, resource.GetType(), JSO);
+ //Reset stream to begining
+ _ = data.Seek(0, SeekOrigin.Begin);
+ //Invoke update callback
+ UpdateCb(this, data);
+ //Clear modified flag
+ Modified = false;
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/VNLib.Utils.csproj b/lib/Utils/src/VNLib.Utils.csproj
new file mode 100644
index 0000000..b14ab27
--- /dev/null
+++ b/lib/Utils/src/VNLib.Utils.csproj
@@ -0,0 +1,47 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <RootNamespace>VNLib.Utils</RootNamespace>
+ <Authors>Vaughn Nugent</Authors>
+ <Product>VNLib Utilities Library</Product>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <AssemblyName>VNLib.Utils</AssemblyName>
+ <Version>1.0.1.10</Version>
+ <Description>Base utilities library, structs, classes</Description>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <Nullable>enable</Nullable>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Include="..\.editorconfig" Link=".editorconfig" />
+ </ItemGroup>
+
+ <PropertyGroup Condition="'$(Configuration)'=='Debug'">
+ <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+</Project>
diff --git a/lib/Utils/src/VnDisposeable.cs b/lib/Utils/src/VnDisposeable.cs
new file mode 100644
index 0000000..4230a13
--- /dev/null
+++ b/lib/Utils/src/VnDisposeable.cs
@@ -0,0 +1,85 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: VnDisposeable.cs
+*
+* VnDisposeable.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace VNLib.Utils
+{
+ /// <summary>
+ /// Provides a base class with abstract methods for for disposable objects, with disposed check method
+ /// </summary>
+ public abstract class VnDisposeable : IDisposable
+ {
+ ///<inheritdoc/>
+ protected bool Disposed { get; private set; }
+
+ /// <summary>
+ /// When overriden in a child class, is responsible for freeing resources
+ /// </summary>
+ protected abstract void Free();
+
+ /// <summary>
+ /// Checks if the current object has been disposed. Method will be inlined where possible
+ /// </summary>
+ /// <exception cref="ObjectDisposedException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected virtual void Check()
+ {
+ if (Disposed)
+ {
+ throw new ObjectDisposedException("Object has been disposed");
+ }
+ }
+
+ /// <summary>
+ /// Sets the internal state to diposed without calling <see cref="Free"/> operation.
+ /// Usefull if another code-path performs the free operation independant of a dispose opreation.
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected void SetDisposedState() => Disposed = true;
+ ///<inheritdoc/>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!Disposed)
+ {
+ if (disposing)
+ {
+ //Call free method
+ Free();
+ }
+ Disposed = true;
+ }
+ }
+ //Finalizer is not needed here
+
+ ///<inheritdoc/>
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Utils/src/VnEncoding.cs b/lib/Utils/src/VnEncoding.cs
new file mode 100644
index 0000000..94d8a1a
--- /dev/null
+++ b/lib/Utils/src/VnEncoding.cs
@@ -0,0 +1,914 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Utils
+* File: VnEncoding.cs
+*
+* VnEncoding.cs is part of VNLib.Utils which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Utils is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Utils is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Text;
+using System.Buffers;
+using System.Text.Json;
+using System.Threading;
+using System.Buffers.Text;
+using System.Threading.Tasks;
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils.IO;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+
+
+namespace VNLib.Utils
+{
+ /// <summary>
+ /// Contains static methods for encoding data
+ /// </summary>
+ public static class VnEncoding
+ {
+ /// <summary>
+ /// Encodes a <see cref="ReadOnlySpan{T}"/> with the specified <see cref="Encoding"/> to a <see cref="VnMemoryStream"/> that must be disposed by the user
+ /// </summary>
+ /// <param name="data">Data to be encoded</param>
+ /// <param name="encoding"><see cref="Encoding"/> to encode data with</param>
+ /// <returns>A <see cref="Stream"/> contating the encoded data</returns>
+ public static VnMemoryStream GetMemoryStream(in ReadOnlySpan<char> data, Encoding encoding)
+ {
+ _ = encoding ?? throw new ArgumentNullException(nameof(encoding));
+ //Create new memory handle to copy data to
+ MemoryHandle<byte>? handle = null;
+ try
+ {
+ //get number of bytes
+ int byteCount = encoding.GetByteCount(data);
+ //resize the handle to fit the data
+ handle = Memory.Memory.Shared.Alloc<byte>(byteCount);
+ //encode
+ int size = encoding.GetBytes(data, handle);
+ //Consume the handle into a new vnmemstream and return it
+ return VnMemoryStream.ConsumeHandle(handle, size, true);
+ }
+ catch
+ {
+ //Dispose the handle if there is an excpetion
+ handle?.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Attempts to deserialze a json object from a stream of UTF8 data
+ /// </summary>
+ /// <typeparam name="T">The type of the object to deserialize</typeparam>
+ /// <param name="data">Binary data to read from</param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param>
+ /// <returns>The object decoded from the stream</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static T? JSONDeserializeFromBinary<T>(Stream? data, JsonSerializerOptions? options = null)
+ {
+ //Return default if null
+ if (data == null)
+ {
+ return default;
+ }
+ //Create a memory stream as a buffer
+ using VnMemoryStream ms = new();
+ //Copy stream data to memory
+ data.CopyTo(ms, null);
+ if (ms.Length > 0)
+ {
+ //Rewind
+ ms.Position = 0;
+ //Recover data from stream
+ return ms.AsSpan().AsJsonObject<T>(options);
+ }
+ //Stream is empty
+ return default;
+ }
+ /// <summary>
+ /// Attempts to deserialze a json object from a stream of UTF8 data
+ /// </summary>
+ /// <param name="data">Binary data to read from</param>
+ /// <param name="type"></param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param>
+ /// <returns>The object decoded from the stream</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static object? JSONDeserializeFromBinary(Stream? data, Type type, JsonSerializerOptions? options = null)
+ {
+ //Return default if null
+ if (data == null)
+ {
+ return default;
+ }
+ //Create a memory stream as a buffer
+ using VnMemoryStream ms = new();
+ //Copy stream data to memory
+ data.CopyTo(ms, null);
+ if (ms.Length > 0)
+ {
+ //Rewind
+ ms.Position = 0;
+ //Recover data from stream
+ return JsonSerializer.Deserialize(ms.AsSpan(), type, options);
+ }
+ //Stream is empty
+ return default;
+ }
+ /// <summary>
+ /// Attempts to deserialze a json object from a stream of UTF8 data
+ /// </summary>
+ /// <typeparam name="T">The type of the object to deserialize</typeparam>
+ /// <param name="data">Binary data to read from</param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param>
+ /// <param name="cancellationToken"></param>
+ /// <returns>The object decoded from the stream</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static ValueTask<T?> JSONDeserializeFromBinaryAsync<T>(Stream? data, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ //Return default if null
+ return data == null || data.Length == 0 ? ValueTask.FromResult<T?>(default) : JsonSerializer.DeserializeAsync<T>(data, options, cancellationToken);
+ }
+ /// <summary>
+ /// Attempts to deserialze a json object from a stream of UTF8 data
+ /// </summary>
+ /// <param name="data">Binary data to read from</param>
+ /// <param name="type"></param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to deserializer</param>
+ /// <param name="cancellationToken"></param>
+ /// <returns>The object decoded from the stream</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static ValueTask<object?> JSONDeserializeFromBinaryAsync(Stream? data, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ //Return default if null
+ return data == null || data.Length == 0 ? ValueTask.FromResult<object?>(default) : JsonSerializer.DeserializeAsync(data, type, options, cancellationToken);
+ }
+ /// <summary>
+ /// Attempts to serialize the object to json and write the encoded data to the stream
+ /// </summary>
+ /// <typeparam name="T">The object type to serialize</typeparam>
+ /// <param name="data">The object to serialize</param>
+ /// <param name="output">The <see cref="Stream"/> to write output data to</param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to serializer</param>
+ /// <exception cref="JsonException"></exception>
+ public static void JSONSerializeToBinary<T>(T data, Stream output, JsonSerializerOptions? options = null)
+ {
+ //return if null
+ if(data == null)
+ {
+ return;
+ }
+ //Serialize
+ JsonSerializer.Serialize(output, data, options);
+ }
+ /// <summary>
+ /// Attempts to serialize the object to json and write the encoded data to the stream
+ /// </summary>
+ /// <param name="data">The object to serialize</param>
+ /// <param name="output">The <see cref="Stream"/> to write output data to</param>
+ /// <param name="type"></param>
+ /// <param name="options"><see cref="JsonSerializerOptions"/> object to pass to serializer</param>
+ /// <exception cref="JsonException"></exception>
+ public static void JSONSerializeToBinary(object data, Stream output, Type type, JsonSerializerOptions? options = null)
+ {
+ //return if null
+ if (data == null)
+ {
+ return;
+ }
+ //Serialize
+ JsonSerializer.Serialize(output, data, type, options);
+ }
+
+ #region Base32
+
+ private const string RFC_4648_BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+ /// <summary>
+ /// Attempts to convert the specified byte sequence in Base32 encoding
+ /// and writing the encoded data to the output buffer.
+ /// </summary>
+ /// <param name="input">The input buffer to convert</param>
+ /// <param name="output">The ouput buffer to write encoded data to</param>
+ /// <returns>The number of characters written, false if no data was written or output buffer was too small</returns>
+ public static ERRNO TryToBase32Chars(ReadOnlySpan<byte> input, Span<char> output)
+ {
+ ForwardOnlyWriter<char> writer = new(output);
+ return TryToBase32Chars(input, ref writer);
+ }
+ /// <summary>
+ /// Attempts to convert the specified byte sequence in Base32 encoding
+ /// and writing the encoded data to the output buffer.
+ /// </summary>
+ /// <param name="input">The input buffer to convert</param>
+ /// <param name="writer">A <see cref="ForwardOnlyWriter{T}"/> to write encoded chars to</param>
+ /// <returns>The number of characters written, false if no data was written or output buffer was too small</returns>
+ public static ERRNO TryToBase32Chars(ReadOnlySpan<byte> input, ref ForwardOnlyWriter<char> writer)
+ {
+ //calculate char size
+ int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
+ //Make sure there is enough room
+ if(charCount > writer.RemainingSize)
+ {
+ return false;
+ }
+ //sliding window over input buffer
+ ForwardOnlyReader<byte> reader = new(input);
+
+ while (reader.WindowSize > 0)
+ {
+ //Convert the current window
+ WriteChars(reader.Window, ref writer);
+ //shift the window
+ reader.Advance(Math.Min(5, reader.WindowSize));
+ }
+ return writer.Written;
+ }
+ private unsafe static void WriteChars(ReadOnlySpan<byte> input, ref ForwardOnlyWriter<char> writer)
+ {
+ //Get the input buffer as long
+ ulong inputAsLong = 0;
+ //Get a byte pointer over the ulong to index it as a byte buffer
+ byte* buffer = (byte*)&inputAsLong;
+ //Check proc endianness
+ if (BitConverter.IsLittleEndian)
+ {
+ //store each byte consecutivley and allow for padding
+ for (int i = 0; (i < 5 && i < input.Length); i++)
+ {
+ //Write bytes from upper to lower byte order for little endian systems
+ buffer[7 - i] = input[i];
+ }
+ }
+ else
+ {
+ //store each byte consecutivley and allow for padding
+ for (int i = 0; (i < 5 && i < input.Length); i++)
+ {
+ //Write bytes from lower to upper byte order for Big Endian systems
+ buffer[i] = input[i];
+ }
+ }
+ int rounds = (input.Length) switch
+ {
+ 1 => 2,
+ 2 => 4,
+ 3 => 5,
+ 4 => 7,
+ _ => 8
+ };
+ //Convert each byte segment up to the number of bytes encoded
+ for (int i = 0; i < rounds; i++)
+ {
+ //store the leading byte
+ byte val = buffer[7];
+ //right shift the value to lower 5 bits
+ val >>= 3;
+ //Lookup charcode
+ char base32Char = RFC_4648_BASE32_CHARS[val];
+ //append the character to the writer
+ writer.Append(base32Char);
+ //Shift input left by 5 bits so the next 5 bits can be read
+ inputAsLong <<= 5;
+ }
+ //Fill remaining bytes with padding chars
+ for(; rounds < 8; rounds++)
+ {
+ //Append trailing '='
+ writer.Append('=');
+ }
+ }
+
+ /// <summary>
+ /// Attempts to decode the Base32 encoded string
+ /// </summary>
+ /// <param name="input">The Base32 encoded data to decode</param>
+ /// <param name="output">The output buffer to write decoded data to</param>
+ /// <returns>The number of bytes written to the output</returns>
+ /// <exception cref="FormatException"></exception>
+ public static ERRNO TryFromBase32Chars(ReadOnlySpan<char> input, Span<byte> output)
+ {
+ ForwardOnlyWriter<byte> writer = new(output);
+ return TryFromBase32Chars(input, ref writer);
+ }
+ /// <summary>
+ /// Attempts to decode the Base32 encoded string
+ /// </summary>
+ /// <param name="input">The Base32 encoded data to decode</param>
+ /// <param name="writer">A <see cref="ForwardOnlyWriter{T}"/> to write decoded bytes to</param>
+ /// <returns>The number of bytes written to the output</returns>
+ /// <exception cref="FormatException"></exception>
+ public unsafe static ERRNO TryFromBase32Chars(ReadOnlySpan<char> input, ref ForwardOnlyWriter<byte> writer)
+ {
+ //TODO support Big-Endian byte order
+
+ //trim padding characters
+ input = input.Trim('=');
+ //Calc the number of bytes to write
+ int outputSize = (input.Length * 5) / 8;
+ //make sure the output buffer is large enough
+ if(writer.RemainingSize < outputSize)
+ {
+ return false;
+ }
+
+ //buffer used to shift data while decoding
+ ulong bufferLong = 0;
+
+ //re-cast to byte* to index it as a byte buffer
+ byte* buffer = (byte*)&bufferLong;
+
+ int count = 0, len = input.Length;
+ while(count < len)
+ {
+ //Convert the character to its char code
+ byte charCode = GetCharCode(input[count]);
+ //write byte to buffer
+ buffer[0] |= charCode;
+ count++;
+ //If 8 characters have been decoded, reset the buffer
+ if((count % 8) == 0)
+ {
+ //Write the 5 upper bytes in reverse order to the output buffer
+ for(int j = 0; j < 5; j++)
+ {
+ writer.Append(buffer[4 - j]);
+ }
+ //reset
+ bufferLong = 0;
+ }
+ //left shift the buffer up by 5 bits
+ bufferLong <<= 5;
+ }
+ //If remaining data has not be written, but has been buffed, finalize it
+ if (writer.Written < outputSize)
+ {
+ //calculate how many bits the buffer still needs to be shifted by (will be 5 bits off because of the previous loop)
+ int remainingShift = (7 - (count % 8)) * 5;
+ //right shift the buffer by the remaining bit count
+ bufferLong <<= remainingShift;
+ //calc remaining bytes
+ int remaining = (outputSize - writer.Written);
+ //Write remaining bytes to the output
+ for(int i = 0; i < remaining; i++)
+ {
+ writer.Append(buffer[4 - i]);
+ }
+ }
+ return writer.Written;
+ }
+ private static byte GetCharCode(char c)
+ {
+ //cast to byte to get its base 10 value
+ return c switch
+ {
+ //Upper case
+ 'A' => 0,
+ 'B' => 1,
+ 'C' => 2,
+ 'D' => 3,
+ 'E' => 4,
+ 'F' => 5,
+ 'G' => 6,
+ 'H' => 7,
+ 'I' => 8,
+ 'J' => 9,
+ 'K' => 10,
+ 'L' => 11,
+ 'M' => 12,
+ 'N' => 13,
+ 'O' => 14,
+ 'P' => 15,
+ 'Q' => 16,
+ 'R' => 17,
+ 'S' => 18,
+ 'T' => 19,
+ 'U' => 20,
+ 'V' => 21,
+ 'W' => 22,
+ 'X' => 23,
+ 'Y' => 24,
+ 'Z' => 25,
+ //Lower case
+ 'a' => 0,
+ 'b' => 1,
+ 'c' => 2,
+ 'd' => 3,
+ 'e' => 4,
+ 'f' => 5,
+ 'g' => 6,
+ 'h' => 7,
+ 'i' => 8,
+ 'j' => 9,
+ 'k' => 10,
+ 'l' => 11,
+ 'm' => 12,
+ 'n' => 13,
+ 'o' => 14,
+ 'p' => 15,
+ 'q' => 16,
+ 'r' => 17,
+ 's' => 18,
+ 't' => 19,
+ 'u' => 20,
+ 'v' => 21,
+ 'w' => 22,
+ 'x' => 23,
+ 'y' => 24,
+ 'z' => 25,
+ //Base10 digits
+ '2' => 26,
+ '3' => 27,
+ '4' => 28,
+ '5' => 29,
+ '6' => 30,
+ '7' => 31,
+
+ _=> throw new FormatException("Character found is not a Base32 encoded character")
+ };
+ }
+
+ /// <summary>
+ /// Calculates the maximum buffer size required to encode a binary block to its Base32
+ /// character encoding
+ /// </summary>
+ /// <param name="bufferSize">The binary buffer size used to calculate the base32 buffer size</param>
+ /// <returns>The maximum size (including padding) of the character buffer required to encode the binary data</returns>
+ public static int Base32CalcMaxBufferSize(int bufferSize)
+ {
+ /*
+ * Base32 encoding consumes 8 bytes for every 5 bytes
+ * of input data
+ */
+ //Add up to 8 bytes for padding
+ return (int)(Math.Ceiling(bufferSize / 5d) * 8) + (8 - (bufferSize % 8));
+ }
+
+ /// <summary>
+ /// Converts the binary buffer to a base32 character string with optional padding characters
+ /// </summary>
+ /// <param name="binBuffer">The buffer to encode</param>
+ /// <param name="withPadding">Should padding be included in the result</param>
+ /// <returns>The base32 encoded string representation of the specified buffer</returns>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static string ToBase32String(ReadOnlySpan<byte> binBuffer, bool withPadding = false)
+ {
+ string value;
+ //Calculate the base32 entropy to alloc an appropriate buffer (minium buffer of 2 chars)
+ int entropy = Base32CalcMaxBufferSize(binBuffer.Length);
+ //Alloc buffer for enough size (2*long bytes) is not an issue
+ using (UnsafeMemoryHandle<char> charBuffer = Memory.Memory.UnsafeAlloc<char>(entropy))
+ {
+ //Encode
+ ERRNO encoded = TryToBase32Chars(binBuffer, charBuffer.Span);
+ if (!encoded)
+ {
+ throw new InternalBufferTooSmallException("Base32 char buffer was too small");
+ }
+ //Convert with or w/o padding
+ if (withPadding)
+ {
+ value = charBuffer.Span[0..(int)encoded].ToString();
+ }
+ else
+ {
+ value = charBuffer.Span[0..(int)encoded].Trim('=').ToString();
+ }
+ }
+ return value;
+ }
+ /// <summary>
+ /// Converts the base32 character buffer to its structure representation
+ /// </summary>
+ /// <typeparam name="T">The structure type</typeparam>
+ /// <param name="base32">The base32 character buffer</param>
+ /// <returns>The new structure of the base32 data</returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static T FromBase32String<T>(ReadOnlySpan<char> base32) where T: unmanaged
+ {
+ //calc size of bin buffer
+ int size = base32.Length;
+ //Rent a bin buffer
+ using UnsafeMemoryHandle<byte> binBuffer = Memory.Memory.UnsafeAlloc<byte>(size);
+ //Try to decode the data
+ ERRNO decoded = TryFromBase32Chars(base32, binBuffer.Span);
+ //Marshal back to a struct
+ return decoded ? MemoryMarshal.Read<T>(binBuffer.Span[..(int)decoded]) : throw new InternalBufferTooSmallException("Binbuffer was too small");
+ }
+
+ /// <summary>
+ /// Gets a byte array of the base32 decoded data
+ /// </summary>
+ /// <param name="base32">The character array to decode</param>
+ /// <returns>The byte[] of the decoded binary data, or null if the supplied character array was empty</returns>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static byte[]? FromBase32String(ReadOnlySpan<char> base32)
+ {
+ if (base32.IsEmpty)
+ {
+ return null;
+ }
+ //Buffer size of the base32 string will always be enough buffer space
+ using UnsafeMemoryHandle<byte> tempBuffer = Memory.Memory.UnsafeAlloc<byte>(base32.Length);
+ //Try to decode the data
+ ERRNO decoded = TryFromBase32Chars(base32, tempBuffer.Span);
+
+ return decoded ? tempBuffer.Span[0..(int)decoded].ToArray() : throw new InternalBufferTooSmallException("Binbuffer was too small");
+ }
+
+ /// <summary>
+ /// Converts a structure to its base32 representation and returns the string of its value
+ /// </summary>
+ /// <typeparam name="T">The structure type</typeparam>
+ /// <param name="value">The structure to encode</param>
+ /// <param name="withPadding">A value indicating if padding should be used</param>
+ /// <returns>The base32 string representation of the structure</returns>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static string ToBase32String<T>(T value, bool withPadding = false) where T : unmanaged
+ {
+ //get the size of the structure
+ int binSize = Unsafe.SizeOf<T>();
+ //Rent a bin buffer
+ Span<byte> binBuffer = stackalloc byte[binSize];
+ //Write memory to buffer
+ MemoryMarshal.Write(binBuffer, ref value);
+ //Convert to base32
+ return ToBase32String(binBuffer, withPadding);
+ }
+
+ #endregion
+
+ #region percent encoding
+
+ private static readonly ReadOnlyMemory<byte> HexToUtf8Pos = new byte[16]
+ {
+ 0x30, //0
+ 0x31, //1
+ 0x32, //2
+ 0x33, //3
+ 0x34, //4
+ 0x35, //5
+ 0x36, //6
+ 0x37, //7
+ 0x38, //8
+ 0x39, //9
+
+ 0x41, //A
+ 0x42, //B
+ 0x43, //C
+ 0x44, //D
+ 0x45, //E
+ 0x46 //F
+ };
+
+ /// <summary>
+ /// Deterimes the size of the buffer needed to encode a utf8 encoded
+ /// character buffer into its url-safe percent/hex encoded representation
+ /// </summary>
+ /// <param name="utf8Bytes">The buffer to examine</param>
+ /// <param name="allowedChars">A sequence of characters that are excluded from encoding</param>
+ /// <returns>The size of the buffer required to encode</returns>
+ public static unsafe int PercentEncodeCalcBufferSize(ReadOnlySpan<byte> utf8Bytes, in ReadOnlySpan<byte> allowedChars = default)
+ {
+ /*
+ * For every illegal character, the percent encoding adds 3 bytes of
+ * entropy. So a single byte will be replaced by 3, so adding
+ * 2 bytes for every illegal character plus the length of the
+ * intial buffer, we get the size of the buffer needed to
+ * percent encode.
+ */
+ int count = 0, len = utf8Bytes.Length;
+ fixed (byte* utfBase = &MemoryMarshal.GetReference(utf8Bytes))
+ {
+ //Find all unsafe characters and add the entropy size
+ for (int i = 0; i < len; i++)
+ {
+ if (!IsUrlSafeChar(utfBase[i], in allowedChars))
+ {
+ count += 2;
+ }
+ }
+ }
+ //Size is initial buffer size + count bytes
+ return len + count;
+ }
+
+ /// <summary>
+ /// Percent encodes the buffer for utf8 encoded characters to its percent/hex encoded
+ /// utf8 character representation
+ /// </summary>
+ /// <param name="utf8Bytes">The buffer of utf8 encoded characters to encode</param>
+ /// <param name="utf8Output">The buffer to write the encoded characters to</param>
+ /// <param name="allowedChars">A sequence of characters that are excluded from encoding</param>
+ /// <returns>The number of characters encoded and written to the output buffer</returns>
+ public static ERRNO PercentEncode(ReadOnlySpan<byte> utf8Bytes, Span<byte> utf8Output, in ReadOnlySpan<byte> allowedChars = default)
+ {
+ int outPos = 0, len = utf8Bytes.Length;
+ ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.Span;
+ for (int i = 0; i < len; i++)
+ {
+ byte value = utf8Bytes[i];
+ //Check if value is url safe
+ if(IsUrlSafeChar(value, in allowedChars))
+ {
+ //Skip
+ utf8Output[outPos++] = value;
+ }
+ else
+ {
+ //Percent encode
+ utf8Output[outPos++] = 0x25; // '%'
+ //Calc and store the encoded by the upper 4 bits
+ utf8Output[outPos++] = lookupTable[(value & 0xf0) >> 4];
+ //Store lower 4 bits in encoded value
+ utf8Output[outPos++] = lookupTable[value & 0x0f];
+ }
+ }
+ //Return the size of the output buffer
+ return outPos;
+ }
+
+ private static bool IsUrlSafeChar(byte value, in ReadOnlySpan<byte> allowedChars)
+ {
+ return
+ // base10 digits
+ value > 0x2f && value < 0x3a
+ // '_' (underscore)
+ || value == 0x5f
+ // '-' (hyphen)
+ || value == 0x2d
+ // Uppercase letters
+ || value > 0x40 && value < 0x5b
+ // lowercase letters
+ || value > 0x60 && value < 0x7b
+ // Check allowed characters
+ || allowedChars.Contains(value);
+ }
+
+ //TODO: Implement decode with better performance, lookup table or math vs searching the table
+
+ /// <summary>
+ /// Decodes a percent (url/hex) encoded utf8 encoded character buffer to its utf8
+ /// encoded binary value
+ /// </summary>
+ /// <param name="utf8Encoded">The buffer containg characters to be decoded</param>
+ /// <param name="utf8Output">The buffer to write deocded values to</param>
+ /// <returns>The nuber of bytes written to the output buffer</returns>
+ /// <exception cref="FormatException"></exception>
+ public static ERRNO PercentDecode(ReadOnlySpan<byte> utf8Encoded, Span<byte> utf8Output)
+ {
+ int outPos = 0, len = utf8Encoded.Length;
+ ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.Span;
+ for (int i = 0; i < len; i++)
+ {
+ byte value = utf8Encoded[i];
+ //Begining of percent encoding character
+ if(value == 0x25)
+ {
+ //Calculate the base16 multiplier from the upper half of the
+ int multiplier = lookupTable.IndexOf(utf8Encoded[i + 1]);
+ //get the base16 lower half to add
+ int lower = lookupTable.IndexOf(utf8Encoded[i + 2]);
+ //Check format
+ if(multiplier < 0 || lower < 0)
+ {
+ throw new FormatException($"Encoded buffer contains invalid hexadecimal characters following the % character at position {i}");
+ }
+ //Calculate the new value, shift multiplier to the upper 4 bits, then mask + or the lower 4 bits
+ value = (byte)(((byte)(multiplier << 4)) | ((byte)lower & 0x0f));
+ //Advance the encoded index by the two consumed chars
+ i += 2;
+ }
+ utf8Output[outPos++] = value;
+ }
+ return outPos;
+ }
+
+ #endregion
+
+ #region Base64
+
+ /// <summary>
+ /// Tries to convert the specified span containing a string representation that is
+ /// encoded with base-64 digits into a span of 8-bit unsigned integers.
+ /// </summary>
+ /// <param name="base64">Base64 character data to recover</param>
+ /// <param name="buffer">The binary output buffer to write converted characters to</param>
+ /// <returns>The number of bytes written, or <see cref="ERRNO.E_FAIL"/> of the conversion was unsucessful</returns>
+ public static ERRNO TryFromBase64Chars(ReadOnlySpan<char> base64, Span<byte> buffer)
+ {
+ return Convert.TryFromBase64Chars(base64, buffer, out int bytesWritten) ? bytesWritten : ERRNO.E_FAIL;
+ }
+ /// <summary>
+ /// Tries to convert the 8-bit unsigned integers inside the specified read-only span
+ /// into their equivalent string representation that is encoded with base-64 digits.
+ /// You can optionally specify whether to insert line breaks in the return value.
+ /// </summary>
+ /// <param name="buffer">The binary buffer to convert characters from</param>
+ /// <param name="base64">The base64 output buffer</param>
+ /// <param name="options">
+ /// One of the enumeration values that specify whether to insert line breaks in the
+ /// return value. The default value is System.Base64FormattingOptions.None.
+ /// </param>
+ /// <returns>The number of characters encoded, or <see cref="ERRNO.E_FAIL"/> if conversion was unsuccessful</returns>
+ public static ERRNO TryToBase64Chars(ReadOnlySpan<byte> buffer, Span<char> base64, Base64FormattingOptions options = Base64FormattingOptions.None)
+ {
+ return Convert.TryToBase64Chars(buffer, base64, out int charsWritten, options: options) ? charsWritten : ERRNO.E_FAIL;
+ }
+
+
+ /*
+ * Calc base64 padding chars excluding the length mod 4 = 0 case
+ * by and-ing 0x03 (011) with the result
+ */
+
+ /// <summary>
+ /// Determines the number of missing padding bytes from the length of the base64
+ /// data sequence.
+ /// <code>
+ /// Formula (4 - (length mod 4) and 0x03
+ /// </code>
+ /// </summary>
+ /// <param name="length">The length of the base64 buffer</param>
+ /// <returns>The number of padding bytes to add to the end of the sequence</returns>
+ public static int Base64CalcRequiredPadding(int length) => (4 - (length % 4)) & 0x03;
+
+ /// <summary>
+ /// Converts a base64 utf8 encoded binary buffer to
+ /// its base64url encoded version
+ /// </summary>
+ /// <param name="base64">The binary buffer to convert</param>
+ public static unsafe void Base64ToUrlSafeInPlace(Span<byte> base64)
+ {
+ int len = base64.Length;
+
+ fixed(byte* ptr = &MemoryMarshal.GetReference(base64))
+ {
+ for (int i = 0; i < len; i++)
+ {
+ switch (ptr[i])
+ {
+ //Replace + with - (minus)
+ case 0x2b:
+ ptr[i] = 0x2d;
+ break;
+ //Replace / with _ (underscore)
+ case 0x2f:
+ ptr[i] = 0x5f;
+ break;
+ }
+ }
+ }
+ }
+ /// <summary>
+ /// Converts a base64url encoded utf8 encoded binary buffer to
+ /// its base64 encoded version
+ /// </summary>
+ /// <param name="uft8Base64Url">The base64url utf8 to decode</param>
+ public static unsafe void Base64FromUrlSafeInPlace(Span<byte> uft8Base64Url)
+ {
+ int len = uft8Base64Url.Length;
+
+ fixed (byte* ptr = &MemoryMarshal.GetReference(uft8Base64Url))
+ {
+ for (int i = 0; i < len; i++)
+ {
+ switch (ptr[i])
+ {
+ //Replace - with + (plus)
+ case 0x2d:
+ ptr[i] = 0x2b;
+ break;
+ //Replace _ with / (slash)
+ case 0x5f:
+ ptr[i] = 0x2f;
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Converts the input buffer to a url safe base64 encoded
+ /// utf8 buffer from the base64 input buffer. The base64 is copied
+ /// directly to the output then converted in place. This is
+ /// just a shortcut method for readonly spans
+ /// </summary>
+ /// <param name="base64">The base64 encoded data</param>
+ /// <param name="base64Url">The base64url encoded output</param>
+ /// <returns>The size of the <paramref name="base64"/> buffer</returns>
+ public static ERRNO Base64ToUrlSafe(ReadOnlySpan<byte> base64, Span<byte> base64Url)
+ {
+ //Aligned copy to the output buffer
+ base64.CopyTo(base64Url);
+ //One time convert the output buffer to url safe
+ Base64ToUrlSafeInPlace(base64Url);
+ return base64.Length;
+ }
+
+ /// <summary>
+ /// Converts the urlsafe input buffer to a base64 encoded
+ /// utf8 buffer from the base64 input buffer. The base64 is copied
+ /// directly to the output then converted in place. This is
+ /// just a shortcut method for readonly spans
+ /// </summary>
+ /// <param name="base64">The base64 encoded data</param>
+ /// <param name="base64Url">The base64url encoded output</param>
+ /// <returns>The size of the <paramref name="base64Url"/> buffer</returns>
+ public static ERRNO Base64FromUrlSafe(ReadOnlySpan<byte> base64Url, Span<byte> base64)
+ {
+ //Aligned copy to the output buffer
+ base64Url.CopyTo(base64);
+ //One time convert the output buffer to url safe
+ Base64FromUrlSafeInPlace(base64);
+ return base64Url.Length;
+ }
+
+ /// <summary>
+ /// Decodes a utf8 base64url encoded sequence of data and writes it
+ /// to the supplied output buffer
+ /// </summary>
+ /// <param name="utf8Base64Url">The utf8 base64 url encoded string</param>
+ /// <param name="output">The output buffer to write the decoded data to</param>
+ /// <returns>The number of bytes written or <see cref="ERRNO.E_FAIL"/> if the operation failed</returns>
+ public static ERRNO Base64UrlDecode(ReadOnlySpan<byte> utf8Base64Url, Span<byte> output)
+ {
+ if(utf8Base64Url.IsEmpty || output.IsEmpty)
+ {
+ return ERRNO.E_FAIL;
+ }
+ //url deocde
+ ERRNO count = Base64FromUrlSafe(utf8Base64Url, output);
+
+ //Writer for adding padding bytes
+ ForwardOnlyWriter<byte> writer = new (output);
+ writer.Advance(count);
+
+ //Calc required padding
+ int paddingToAdd = Base64CalcRequiredPadding(writer.Written);
+ //Add padding bytes
+ for (; paddingToAdd > 0; paddingToAdd--)
+ {
+ writer.Append(0x3d); // '='
+ }
+
+ //Base64 decode in place, we should have a buffer large enough
+ OperationStatus status = Base64.DecodeFromUtf8InPlace(writer.AsSpan(), out int bytesWritten);
+ //If status is successful return the number of bytes written
+ return status == OperationStatus.Done ? bytesWritten : ERRNO.E_FAIL;
+ }
+
+ /// <summary>
+ /// Decodes a base64url encoded character sequence
+ /// of data and writes it to the supplied output buffer
+ /// </summary>
+ /// <param name="chars">The character buffer to decode</param>
+ /// <param name="output">The output buffer to write decoded data to</param>
+ /// <param name="encoding">The character encoding</param>
+ /// <returns>The number of bytes written or <see cref="ERRNO.E_FAIL"/> if the operation failed</returns>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static ERRNO Base64UrlDecode(ReadOnlySpan<char> chars, Span<byte> output, Encoding? encoding = null)
+ {
+ if (chars.IsEmpty || output.IsEmpty)
+ {
+ return ERRNO.E_FAIL;
+ }
+ //Set the encoding to utf8
+ encoding ??= Encoding.UTF8;
+ //get the number of bytes to alloc a buffer
+ int decodedSize = encoding.GetByteCount(chars);
+
+ //alloc buffer
+ using UnsafeMemoryHandle<byte> decodeHandle = Memory.Memory.UnsafeAlloc<byte>(decodedSize);
+ //Get the utf8 binary data
+ int count = encoding.GetBytes(chars, decodeHandle);
+ return Base64UrlDecode(decodeHandle.Span[..count], output);
+ }
+
+ #endregion
+ }
+} \ No newline at end of file