aboutsummaryrefslogtreecommitdiff
path: root/lib/Utils/src/Extensions
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Utils/src/Extensions')
-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
13 files changed, 3381 insertions, 0 deletions
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