From b679ddd4e647ac915febd0d5a5e488a1e8e48842 Mon Sep 17 00:00:00 2001 From: vnugent Date: Thu, 29 Feb 2024 21:23:26 -0500 Subject: Squashed commit of the following: commit 231e26e5c6731e6e156d7c0591518e84a3b82f5a Author: vnugent Date: Thu Feb 29 20:59:42 2024 -0500 fix: #5 find and patch Windows compression bug and some deployment commit d0bfe14e0a0e27172b8dd41f468265e651784837 Author: vnugent Date: Wed Feb 21 21:39:19 2024 -0500 fix: #4 fix readme licensing message for accuracy commit 6f37f152fcd105e40af6689192a36b87eda95f51 Author: vnugent Date: Wed Feb 21 21:37:55 2024 -0500 fix: jwt hashalg sizing and public api for hashalg sizes --- README.md | 2 +- .../src/IdentityUtility/HashingExtensions.cs | 7 + .../src/IdentityUtility/JwtExtensions.cs | 10 +- lib/Hashing.Portable/src/ManagedHash.cs | 57 ++-- lib/Hashing.Portable/src/ManagedHashAlgImpl.cs | 29 +- lib/Hashing.Portable/tests/ManagedHashTests.cs | 81 ++++++ .../VNLib.Net.Compression/CompressorManager.cs | 33 ++- .../CompressorManagerTests.cs | 46 ++-- lib/Net.Compression/vnlib_compress/CMakeLists.txt | 26 +- lib/Net.Compression/vnlib_compress/Taskfile.yaml | 5 +- lib/Net.Compression/vnlib_compress/compression.c | 67 ++--- .../vnlib_compress/feature_brotli.c | 3 +- .../vnlib_compress/feature_brotli.h | 2 +- lib/Net.Compression/vnlib_compress/util.h | 82 +++--- .../Core/Buffering/ContextLockedBufferManager.cs | 12 +- .../src/Core/Buffering/IHttpBufferManager.cs | 10 +- .../src/Core/Compression/ManagedHttpCompressor.cs | 21 +- lib/Net.Http/src/Core/HttpContext.cs | 26 +- lib/Net.Http/src/Core/HttpServerBase.cs | 32 +-- lib/Net.Http/src/Core/Request/HttpInputStream.cs | 27 +- .../src/Core/Response/ChunkDataAccumulator.cs | 52 +--- .../Core/Response/HttpContextResponseWriting.cs | 14 +- lib/Net.Http/src/Core/Response/HttpResponse.cs | 44 +-- .../src/Core/Response/HttpStreamResponse.cs | 50 ++++ .../src/Core/Response/HttpstreamResponse.cs | 50 ++++ .../src/Core/Response/ResponsBodyDataState.cs | 100 +++++++ lib/Net.Http/src/Core/Response/ResponseWriter.cs | 298 ++++++++------------- .../src/Accounts/UserCreationRequest.cs | 16 +- lib/Plugins.Essentials/src/EventProcessor.cs | 18 +- .../src/Extensions/ConnectionInfoExtensions.cs | 1 + .../src/Extensions/EssentialHttpEventExtensions.cs | 98 +++++-- .../src/SemiConsistentVeTable.cs | 25 +- .../src/Sessions/ISessionExtensions.cs | 4 +- .../src/Users/IUserCreationRequest.cs | 4 +- lib/Utils/src/IO/VnMemoryStream.cs | 18 +- lib/Utils/src/Memory/Caching/IReusable.cs | 4 +- lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs | 7 +- lib/Utils/src/Memory/MemoryUtil.cs | 38 +-- lib/Utils/src/Memory/ProcessHeap.cs | 14 +- lib/Utils/src/Native/SafeLibraryHandle.cs | 20 +- .../src/Loader/ManagedLoadContext.cs | 4 +- 41 files changed, 847 insertions(+), 610 deletions(-) create mode 100644 lib/Net.Http/src/Core/Response/HttpStreamResponse.cs create mode 100644 lib/Net.Http/src/Core/Response/HttpstreamResponse.cs create mode 100644 lib/Net.Http/src/Core/Response/ResponsBodyDataState.cs diff --git a/README.md b/README.md index f33f19e..cf36072 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ VNLib is a collection of cross platform .NET/C#/C libraries that I maintain for Any libraries in this repository that contain external dependencies will be mentioned explicitly in the library's readme. I intend to limit this behavior, as it is the reason this repository exists. ### Licensing -Projects contained in this repository are individually licensed, either GNU GPL V2+ or GNU GPL Affero V3+. Builds contain the required license txt files in the archive. Reasoning: some projects are expected for server use, that's really about it, I prefer the the GPL v2 license for other applications where usage will be more broad. For example, the [Utils](lib/Utils/#) library is used for all sorts of projects. +Projects contained in this repository are individually licensed, usually GNU copy-left but some are more open, or are modified and/or vendored projects. Builds will contain the required license txt files (along with third-party licenses) in the archive. ### Documentation Docs and articles will be available from the docs link below . There are docs per project (as they are added) and docs tagged for the VNLib.Core module. I will also be adding "spec" or specification articles to explain the higher level concepts behind the design. diff --git a/lib/Hashing.Portable/src/IdentityUtility/HashingExtensions.cs b/lib/Hashing.Portable/src/IdentityUtility/HashingExtensions.cs index 2a18cfc..12ba095 100644 --- a/lib/Hashing.Portable/src/IdentityUtility/HashingExtensions.cs +++ b/lib/Hashing.Portable/src/IdentityUtility/HashingExtensions.cs @@ -193,6 +193,13 @@ namespace VNLib.Hashing.IdentityUtility return !alg.TryEncrypt(buffer.Span, output, padding, out int bytesWritten) ? ERRNO.E_FAIL : (ERRNO)bytesWritten; } + /// + /// Gets the hash size in bytes for the current value + /// + /// + /// The size (in bytes) of the algorithm's hash + public static int HashSize(this HashAlg alg) => ManagedHash.GetHashSize(alg); + /// /// Gets the for the current /// value. diff --git a/lib/Hashing.Portable/src/IdentityUtility/JwtExtensions.cs b/lib/Hashing.Portable/src/IdentityUtility/JwtExtensions.cs index ef6715b..68033cd 100644 --- a/lib/Hashing.Portable/src/IdentityUtility/JwtExtensions.cs +++ b/lib/Hashing.Portable/src/IdentityUtility/JwtExtensions.cs @@ -275,7 +275,7 @@ namespace VNLib.Hashing.IdentityUtility public static void Sign(this JsonWebToken jwt, ReadOnlySpan key, HashAlg alg) { //Stack hash output buffer, will be the size of the alg - Span sigOut = stackalloc byte[(int)alg]; + Span sigOut = stackalloc byte[alg.HashSize()]; //Compute ERRNO count = ManagedHash.ComputeHmac(key, jwt.HeaderAndPayload, sigOut, alg); @@ -394,7 +394,7 @@ namespace VNLib.Hashing.IdentityUtility int sigBufSize = CalcPadding(signature.Length) + signature.Length; //Calc full buffer size - nint bufferSize = MemoryUtil.NearestPage(sigBufSize + (int)alg); + nint bufferSize = MemoryUtil.NearestPage(sigBufSize + alg.HashSize()); //Alloc buffer to decode data, as a full page, all buffers will be used from the block for better cache using UnsafeMemoryHandle buffer = jwt.Heap.UnsafeAlloc((int)bufferSize); @@ -442,10 +442,10 @@ namespace VNLib.Hashing.IdentityUtility /// public static bool Verify(this JsonWebToken jwt, ReadOnlySpan key, HashAlg alg) { - _ = jwt ?? throw new ArgumentNullException(nameof(jwt)); + ArgumentNullException.ThrowIfNull(jwt); //Get base64 buffer size for in-place conversion - int bufferSize = Base64.GetMaxEncodedToUtf8Length((int)alg); + int bufferSize = Base64.GetMaxEncodedToUtf8Length(alg.HashSize()); //Alloc buffer for signature output Span signatureBuffer = stackalloc byte[bufferSize]; @@ -458,7 +458,7 @@ namespace VNLib.Hashing.IdentityUtility } //Do an in-place base64 conversion of the signature to base64url - ERRNO encoded = VnEncoding.Base64UrlEncodeInPlace(signatureBuffer, (int)alg, false); + ERRNO encoded = VnEncoding.Base64UrlEncodeInPlace(signatureBuffer, alg.HashSize(), false); if (!encoded) { diff --git a/lib/Hashing.Portable/src/ManagedHash.cs b/lib/Hashing.Portable/src/ManagedHash.cs index 7791eed..429660f 100644 --- a/lib/Hashing.Portable/src/ManagedHash.cs +++ b/lib/Hashing.Portable/src/ManagedHash.cs @@ -29,7 +29,6 @@ using System.Security.Cryptography; using VNLib.Utils; using VNLib.Utils.Memory; -using VNLib.Hashing.Native.MonoCypher; namespace VNLib.Hashing { @@ -84,7 +83,7 @@ namespace VNLib.Hashing /// Gets a value that indicates whether the current runtime has the required libraries /// available to support the Blake2b hashing algorithm /// - public static bool SupportsBlake2b => MonoCypherLibrary.CanLoadDefaultLibrary(); + public static bool SupportsBlake2b => IsAlgSupported(HashAlg.BlAKE2B); /// /// Gets a value that indicates whether the current platform supports the SHA3 @@ -103,8 +102,8 @@ namespace VNLib.Hashing HashAlg.SHA3_512 => Sha3_512.IsSupported, HashAlg.SHA3_384 => Sha3_384.IsSupported, HashAlg.SHA3_256 => Sha3_256.IsSupported, - HashAlg.BlAKE2B => SupportsBlake2b, - HashAlg.SHA512 => true, + HashAlg.BlAKE2B => Blake2b.IsSupported, + HashAlg.SHA512 => true, //Built-in functions are always supported HashAlg.SHA384 => true, HashAlg.SHA256 => true, HashAlg.SHA1 => true, @@ -112,6 +111,26 @@ namespace VNLib.Hashing _ => false }; + /// + /// Gets the size of the hash (in bytes) for the specified algorithm + /// + /// The hash algorithm to get the size of + /// A positive 32-bit integer size (in bytes) of the algorithm hash size + /// + public static int GetHashSize(HashAlg type) => type switch + { + HashAlg.SHA3_512 => _3_sha512.HashSize, + HashAlg.SHA3_384 => _3_sha384.HashSize, + HashAlg.SHA3_256 => _3_sha256.HashSize, + HashAlg.BlAKE2B => _blake2bAlg.HashSize, + HashAlg.SHA512 => _sha512Alg.HashSize, + HashAlg.SHA384 => _sha384Alg.HashSize, + HashAlg.SHA256 => _sha256Alg.HashSize, + HashAlg.SHA1 => _sha1Alg.HashSize, + HashAlg.MD5 => _md5Alg.HashSize, + _ => throw new ArgumentException("Invalid hash algorithm", nameof(type)) + }; + /// /// Uses the UTF8 character encoding to encode the string, then /// attempts to compute the hash and store the results into the output buffer @@ -265,11 +284,11 @@ namespace VNLib.Hashing private static byte[] ComputeHashInternal(HashAlg alg, ReadOnlySpan data, ReadOnlySpan key = default) { //Alloc output buffer - byte[] output = new byte[HashSize(alg)]; + byte[] output = new byte[GetHashSize(alg)]; //Hash data ERRNO result = ComputeHashInternal(alg, data, output, key); - Debug.Assert(result == HashSize(alg), $"Failed to compute hash using {alg} of size {output.Length}"); + Debug.Assert(result == GetHashSize(alg), $"Failed to compute hash using {alg} of size {output.Length}"); return output; } @@ -288,13 +307,13 @@ namespace VNLib.Hashing private static string ComputeHashInternal(HashAlg alg, ReadOnlySpan data, HashEncodingMode mode, ReadOnlySpan key = default) { //Alloc stack buffer to store hash output - Span hashBuffer = stackalloc byte[HashSize(alg)]; + Span hashBuffer = stackalloc byte[GetHashSize(alg)]; //hash the buffer ERRNO count = ComputeHashInternal(alg, data, hashBuffer, key); //Count should always be the same as the hash size, this should never fail - Debug.Assert(count == HashSize(alg), $"Failed to compute hash using {alg} of size {hashBuffer.Length}"); + Debug.Assert(count == GetHashSize(alg), $"Failed to compute hash using {alg} of size {hashBuffer.Length}"); //Convert to encoded string return mode switch @@ -310,13 +329,13 @@ namespace VNLib.Hashing private static string ComputeHashInternal(HashAlg alg, ReadOnlySpan data, HashEncodingMode mode, ReadOnlySpan key = default) { //Alloc stack buffer to store hash output - Span hashBuffer = stackalloc byte[HashSize(alg)]; + Span hashBuffer = stackalloc byte[GetHashSize(alg)]; //hash the buffer ERRNO count = ComputeHashInternal(alg, data, hashBuffer, key); //Count should always be the same as the hash size, this should never fail - Debug.Assert(count == HashSize(alg), $"Failed to compute hash using {alg} of size {hashBuffer.Length}"); + Debug.Assert(count == GetHashSize(alg), $"Failed to compute hash using {alg} of size {hashBuffer.Length}"); //Convert to encoded string return mode switch @@ -333,29 +352,15 @@ namespace VNLib.Hashing private static byte[] ComputeHashInternal(HashAlg alg, ReadOnlySpan data, ReadOnlySpan key = default) { //Alloc output buffer - byte[] output = new byte[HashSize(alg)]; + byte[] output = new byte[GetHashSize(alg)]; //Hash data ERRNO result = ComputeHashInternal(alg, data, output, key); - Debug.Assert(result == HashSize(alg), $"Failed to compute hash using {alg} of size {output.Length}"); + Debug.Assert(result == GetHashSize(alg), $"Failed to compute hash using {alg} of size {output.Length}"); return output; } - private static int HashSize(HashAlg alg) => alg switch - { - HashAlg.SHA3_512 => _3_sha512.HashSize, - HashAlg.SHA3_384 => _3_sha384.HashSize, - HashAlg.SHA3_256 => _3_sha256.HashSize, - HashAlg.BlAKE2B => _blake2bAlg.HashSize, - HashAlg.SHA512 => _sha512Alg.HashSize, - HashAlg.SHA384 => _sha384Alg.HashSize, - HashAlg.SHA256 => _sha256Alg.HashSize, - HashAlg.SHA1 => _sha1Alg.HashSize, - HashAlg.MD5 => _md5Alg.HashSize, - _ => throw new ArgumentException("Invalid hash algorithm", nameof(alg)) - }; - private static ERRNO ComputeHashInternal(HashAlg alg, ReadOnlySpan data, Span buffer, ReadOnlySpan key = default) { diff --git a/lib/Hashing.Portable/src/ManagedHashAlgImpl.cs b/lib/Hashing.Portable/src/ManagedHashAlgImpl.cs index d53152e..eae8903 100644 --- a/lib/Hashing.Portable/src/ManagedHashAlgImpl.cs +++ b/lib/Hashing.Portable/src/ManagedHashAlgImpl.cs @@ -150,6 +150,8 @@ namespace VNLib.Hashing internal static int MaxHashSize => MCBlake2Module.MaxHashSize; internal static int MaxKeySize => MCBlake2Module.MaxKeySize; + public static bool IsSupported => MonoCypherLibrary.CanLoadDefaultLibrary(); + /// public readonly int HashSize => DefaultBlake2HashSize; @@ -180,9 +182,18 @@ namespace VNLib.Hashing { count = 0; - if (output.Length > MCBlake2Module.MaxHashSize) + /* + * For this specific implementation, the ManagedHash class assumes hashing implementations + * have a fixed hash size. This is not the case for Blake2b, so we need to enforce one. + * + * Since the HasSize property is publicly accessible users can assume the hash size is + * the value returned by the property. So it should be safe to enforce the hash size here. + * + * Any data outside the default hash size is not our problem. + */ + if (output.Length > HashSize) { - return false; + output = output[..HashSize]; } //Test key size @@ -191,17 +202,11 @@ namespace VNLib.Hashing return false; } - //Compute one-shot hash - ERRNO result = MonoCypherLibrary.Shared.Blake2ComputeHmac(key, data, output); + //Compute one-shot hash, a variable length result may occur + count = MonoCypherLibrary.Shared.Blake2ComputeHmac(key, data, output); - if (result < output.Length) - { - count = 0; - return false; - } - - count = output.Length; - return true; + //Output should always be the same size as the hash size + return count == output.Length; } } } diff --git a/lib/Hashing.Portable/tests/ManagedHashTests.cs b/lib/Hashing.Portable/tests/ManagedHashTests.cs index a9fa467..3bcdb33 100644 --- a/lib/Hashing.Portable/tests/ManagedHashTests.cs +++ b/lib/Hashing.Portable/tests/ManagedHashTests.cs @@ -97,5 +97,86 @@ namespace VNLib.Hashing.Tests Assert.IsTrue(testEnc.Length == Math.Abs(hashSize) * 2); } } + + const string TestHashInput = "Hello World!"; + static readonly string[] TestHashOutputHex = + [ + //None + "", + //Md5 + "ed076287532e86365e841e92bfc50d8c", + //Sha1 + "2ef7bde608ce5404e97d5f042f95f89f1c232871", + //Sha2 + "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", + "bfd76c0ebbd006fee583410547c1887b0292be76d582d96c242d2a792723e3fd6fd061f9d5cfd13b8f961358e6adba4a", + "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8", + + //Sha3 + "d0e47486bbf4c16acac26f8b653592973c1362909f90262877089f9c8a4536af", + "f324cbd421326a2abaedf6f395d1a51e189d4a71c755f531289e519f079b224664961e385afcc37da348bd859f34fd1c", + "32400b5e89822de254e8d5d94252c52bdcb27a3562ca593e980364d9848b8041b98eabe16c1a6797484941d2376864a1b0e248b0f7af8b1555a778c336a5bf48", + + //Blake2b (64 bytes/512 bits) + "54b113f499799d2f3c0711da174e3bc724737ad18f63feb286184f0597e1466436705d6c8e8c7d3d3b88f5a22e83496e0043c44a3c2b1700e0e02259f8ac468e" + ]; + + //Known hash sizes to compare against + static readonly int[] HashSizes = + [ + 0, + 16, + 20, + 32, + 48, + 64, + 32, + 48, + 64, + 64 + ]; + + [TestMethod()] + public void ComputeHexHashTest() + { + HashAlg[] algs = Enum.GetValues(); + + Assert.AreEqual(algs.Length, TestHashOutputHex.Length); + + for (int i = 0; i < algs.Length; i++) + { + if (algs[i] == HashAlg.None) + continue; + + //Skip unsupported algorithms + if (!ManagedHash.IsAlgSupported(algs[i])) + continue; + + string hash = ManagedHash.ComputeHash(TestHashInput, algs[i], HashEncodingMode.Hexadecimal); + + //Make sure exact length (x2 for hex) + Assert.AreEqual(HashSizes[i] * 2, hash.Length); + + //Make sure exact value + Assert.AreEqual(TestHashOutputHex[i], hash, true); + } + } + + [TestMethod()] + public void AlgSizeTest() + { + HashAlg[] algs = Enum.GetValues(); + + for (int i = 0; i < algs.Length; i++) + { + if (algs[i] == HashAlg.None) + continue; + + //Make sure exact length + Assert.AreEqual(HashSizes[i], ManagedHash.GetHashSize(algs[i])); + } + + Assert.ThrowsException(() => ManagedHash.GetHashSize(HashAlg.None)); + } } } \ No newline at end of file diff --git a/lib/Net.Compression/VNLib.Net.Compression/CompressorManager.cs b/lib/Net.Compression/VNLib.Net.Compression/CompressorManager.cs index a614ab8..efbee77 100644 --- a/lib/Net.Compression/VNLib.Net.Compression/CompressorManager.cs +++ b/lib/Net.Compression/VNLib.Net.Compression/CompressorManager.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Compression @@ -111,7 +111,8 @@ namespace VNLib.Net.Compression /// public int InitCompressor(object compressorState, CompressionMethod compMethod) { - Compressor compressor = Unsafe.As(compressorState) ?? throw new ArgumentNullException(nameof(compressorState)); + DebugThrowIfNull(compressorState, nameof(compressorState)); + Compressor compressor = Unsafe.As(compressorState); //Instance should be null during initialization calls Debug.Assert(compressor.Instance == IntPtr.Zero, "Init was called but and old compressor instance was not properly freed"); @@ -126,7 +127,8 @@ namespace VNLib.Net.Compression /// public void DeinitCompressor(object compressorState) { - Compressor compressor = Unsafe.As(compressorState) ?? throw new ArgumentNullException(nameof(compressorState)); + DebugThrowIfNull(compressorState, nameof(compressorState)); + Compressor compressor = Unsafe.As(compressorState); if(compressor.Instance == IntPtr.Zero) { @@ -143,7 +145,8 @@ namespace VNLib.Net.Compression /// public int Flush(object compressorState, Memory output) { - Compressor compressor = Unsafe.As(compressorState) ?? throw new ArgumentNullException(nameof(compressorState)); + DebugThrowIfNull(compressorState, nameof(compressorState)); + Compressor compressor = Unsafe.As(compressorState); if (compressor.Instance == IntPtr.Zero) { @@ -151,14 +154,15 @@ namespace VNLib.Net.Compression } //Force a flush until no more data is available - CompressionResult result = _nativeLib.CompressBlock(compressor.Instance, output, default, true); + CompressionResult result = _nativeLib!.CompressBlock(compressor.Instance, output, default, true); return result.BytesWritten; } /// public CompressionResult CompressBlock(object compressorState, ReadOnlyMemory input, Memory output) { - Compressor compressor = Unsafe.As(compressorState) ?? throw new ArgumentNullException(nameof(compressorState)); + DebugThrowIfNull(compressorState, nameof(compressorState)); + Compressor compressor = Unsafe.As(compressorState); if (compressor.Instance == IntPtr.Zero) { @@ -166,9 +170,20 @@ namespace VNLib.Net.Compression } //Compress the block - return _nativeLib.CompressBlock(compressor.Instance, output, input, false); - } - + return _nativeLib!.CompressBlock(compressor.Instance, output, input, false); + } + + /* + * This compressor manager instance is designed to tbe used by a webserver instance, + * (or multiple) as internal calls. We can assume the library compression calls + * are "correct" and should never pass null objects to these function calls. + * + * Its also a managed type and if null will still raise a null ref exception + * if the instances are null. This is just cleaner for debugging purposes. + */ + + [Conditional("DEBUG")] + private static void DebugThrowIfNull(T? obj, string name) => ArgumentNullException.ThrowIfNull(obj, name); /* * A class to contain the compressor state diff --git a/lib/Net.Compression/VNLib.Net.CompressionTests/CompressorManagerTests.cs b/lib/Net.Compression/VNLib.Net.CompressionTests/CompressorManagerTests.cs index 95c1cc1..236d39c 100644 --- a/lib/Net.Compression/VNLib.Net.CompressionTests/CompressorManagerTests.cs +++ b/lib/Net.Compression/VNLib.Net.CompressionTests/CompressorManagerTests.cs @@ -26,7 +26,7 @@ namespace VNLib.Net.Compression.Tests public void NativeLibApiTest() { //Load library - using NativeCompressionLib lib = NativeCompressionLib.LoadLibrary(LIB_PATH, System.Runtime.InteropServices.DllImportSearchPath.SafeDirectories); + using NativeCompressionLib lib = NativeCompressionLib.LoadLibrary(LIB_PATH, DllImportSearchPath.SafeDirectories); LibTestComp cp = new(lib, CompressionLevel.Fastest); @@ -71,7 +71,7 @@ namespace VNLib.Net.Compression.Tests PrintSystemInformation(); //Load native library - using NativeCompressionLib lib = NativeCompressionLib.LoadLibrary(LIB_PATH, System.Runtime.InteropServices.DllImportSearchPath.SafeDirectories); + using NativeCompressionLib lib = NativeCompressionLib.LoadLibrary(LIB_PATH, DllImportSearchPath.SafeDirectories); //Huge array of random data to compress byte[] testData = RandomNumberGenerator.GetBytes(10 * 1024 * 1024); @@ -102,7 +102,7 @@ namespace VNLib.Net.Compression.Tests private static void TestSingleCompressor(LibTestComp comp, CompressionMethod method, CompressionLevel level, byte[] testData) { byte[] outputBlock = new byte[8 * 1024]; - long ms; + long nativeTicks; Stopwatch stopwatch = new (); { @@ -132,11 +132,13 @@ namespace VNLib.Net.Compression.Tests { //Include deinit comp.DeinitCompressor(); - stopwatch.Stop(); - ms = stopwatch.ElapsedTicks / (TimeSpan.TicksPerMillisecond / 1000); + stopwatch.Stop(); } } + nativeTicks = stopwatch.ElapsedTicks; + + //Switch to managed test using (Stream compStream = GetEncodeStream(Stream.Null, method, level)) { stopwatch.Restart(); @@ -151,13 +153,16 @@ namespace VNLib.Net.Compression.Tests } } - long streamMicroseconds = stopwatch.ElapsedTicks / (TimeSpan.TicksPerMillisecond / 1000); + long streamMicroseconds = TicksToMicroseconds(stopwatch.ElapsedTicks); + long nativeMicroseconds = TicksToMicroseconds(nativeTicks); - string winner = ms < streamMicroseconds ? "native" : "stream"; + string winner = nativeMicroseconds < streamMicroseconds ? "native" : "stream"; - Debug.WriteLine($"{method}: {testData.Length} bytes, {ms}misec vs {streamMicroseconds}misec. Winner {winner}"); + Debug.WriteLine($"{method}: {testData.Length} bytes, {nativeMicroseconds}misec vs {streamMicroseconds}misec. Winner {winner}"); } + static long TicksToMicroseconds(long ticks) => ticks / (TimeSpan.TicksPerMillisecond / 1000); + private static CompressorManager InitCompressorUnderTest() { CompressorManager manager = new(); @@ -255,16 +260,17 @@ namespace VNLib.Net.Compression.Tests Debug.WriteLine($"Compressor library supports {supported}"); } + /* + * This test method initalizes a new compressor instance of the desired type + * creates a test data buffer, compresses it using the compressor instance + * then decompresses the compressed data using a managed decompressor as + * a reference and compares the results. + * + * The decompression must be able to recover the original data. + */ + private static void TestCompressorMethod(ITestCompressor compressor, CompressionMethod method) - { - /* - * This test method initalizes a new compressor instance of the desired type - * creates a test data buffer, compresses it using the compressor instance - * then decompresses the compressed data using a managed decompressor as - * a reference and compares the results. - * - * The decompression must be able to recover the original data. - */ + { //Time to initialize the compressor int blockSize = compressor.InitCompressor(method); @@ -311,7 +317,7 @@ namespace VNLib.Net.Compression.Tests outputStream.Write(output.AsSpan()[0..flushed]); } - //Verify the data + //Verify the original data matches the decompressed data byte[] decompressed = DecompressData(outputStream, method); Assert.IsTrue(buffer.SequenceEqual(decompressed)); @@ -393,7 +399,7 @@ Page Size: {Environment.SystemPageSize} CompressionMethod GetSupportedMethods(); } - sealed record class ManagerTestComp(object Compressor, CompressorManager Manager) : ITestCompressor + sealed class ManagerTestComp(object Compressor, CompressorManager Manager) : ITestCompressor { public CompressionResult CompressBlock(ReadOnlyMemory input, Memory output) => Manager.CompressBlock(Compressor, input, output); @@ -407,7 +413,7 @@ Page Size: {Environment.SystemPageSize} } - sealed record class LibTestComp(NativeCompressionLib Library, CompressionLevel Level) : ITestCompressor + sealed class LibTestComp(NativeCompressionLib Library, CompressionLevel Level) : ITestCompressor { private INativeCompressor? _comp; diff --git a/lib/Net.Compression/vnlib_compress/CMakeLists.txt b/lib/Net.Compression/vnlib_compress/CMakeLists.txt index 17b01ca..977b9a1 100644 --- a/lib/Net.Compression/vnlib_compress/CMakeLists.txt +++ b/lib/Net.Compression/vnlib_compress/CMakeLists.txt @@ -218,10 +218,16 @@ if(ENABLE_ZLIB) check_c_compiler_flag(/arch:AVX HAS_AVX) if (HAS_AVX) list(APPEND Z_C_FLAGS /arch:AVX) + list(APPEND ZLIB_DEFINITIONS -DHAS_AVX) endif() - elseif(UNIX) + #All x64 machines have SSE2, so we can use it as + #and the Windows compiler will automatically use it + #so we only need to set the definition + list(APPEND Z_C_FLAGS /arch:SSE2) + list(APPEND ZLIB_DEFINITIONS -DHAS_SSE2 -DHAS_SSE42) + elseif(UNIX) #for cloudflare intrinsic detections check_c_compiler_flag(-march=armv8-a+crc ARM_CRC) @@ -229,9 +235,15 @@ if(ENABLE_ZLIB) check_c_compiler_flag(-mssse3 HAS_SSSE3) check_c_compiler_flag(-msse4.2 HAS_SSE42) check_c_compiler_flag(-mpclmul HAS_PCLMUL) - + if(ARM_CRC) list(APPEND Z_C_FLAGS -march=armv8-a+crc) + + if(EXISTS "../third-party/zlib/adler32_simd.c") + list(APPEND ZLIB_DEFINITIONS -DADLER32_SIMD_NEON) + list(APPEND ZLIB_SOURCES ../third-party/zlib/adler32_simd.c) + endif() + else() if(HAS_SSE2) list(APPEND Z_C_FLAGS -msse2) @@ -241,7 +253,7 @@ if(ENABLE_ZLIB) if(HAS_SSSE3) list(APPEND Z_C_FLAGS -mssse3) - #add cloudflare intrinsic optimizations + #add cloudflare intrinsic optimizations, may not be present if using non-cloudflare fork if(EXISTS "../third-party/zlib/adler32_simd.c") list(APPEND ZLIB_DEFINITIONS -DHAS_SSSE3 -DADLER32_SIMD_SSSE3) list(APPEND ZLIB_SOURCES ../third-party/zlib/adler32_simd.c) @@ -255,7 +267,7 @@ if(ENABLE_ZLIB) if(HAS_PCLMUL) list(APPEND Z_C_FLAGS -mpclmul) - #add cloudflare intrinsic optimizations for PCMLONGMUL + #add cloudflare intrinsic optimizations for PCMLONGMUL crc32, may not be present if using non-cloudflare fork if(EXISTS "../third-party/zlib/crc32_simd.c") list(APPEND ZLIB_DEFINITIONS -DHAS_PCLMUL) list(APPEND ZLIB_SOURCES ../third-party/zlib/crc32_simd.c) @@ -263,9 +275,9 @@ if(ENABLE_ZLIB) endif() endif() - - endif() + + #add zlib as a library to link later add_library(lib_deflate STATIC ${ZLIB_SOURCES}) @@ -278,7 +290,7 @@ if(ENABLE_ZLIB) #add the definitions to the zlib project only target_compile_definitions(lib_deflate PRIVATE ${ZLIB_DEFINITIONS}) - #only target zlib project + #only target zlib project with compiler flags target_compile_options(lib_deflate PRIVATE ${Z_C_FLAGS}) target_link_libraries(${CMAKE_PROJECT_NAME} lib_deflate) diff --git a/lib/Net.Compression/vnlib_compress/Taskfile.yaml b/lib/Net.Compression/vnlib_compress/Taskfile.yaml index 5271061..28b3ff0 100644 --- a/lib/Net.Compression/vnlib_compress/Taskfile.yaml +++ b/lib/Net.Compression/vnlib_compress/Taskfile.yaml @@ -21,7 +21,11 @@ tasks: #make third-party dir before cloning libs - cmd: powershell -Command "mkdir '{{.THIRD_PARTY_DIR}}' -Force" + platforms: [windows] ignore_error: true + - cmd: mkdir -p '{{.THIRD_PARTY_DIR}}' + platforms: [linux, darwin] + ignore_error: true - task: zlib - task: brotli @@ -51,7 +55,6 @@ tasks: internal: true status: - cd {{.THIRD_PARTY_DIR}} && git clone https://github.com/cloudflare/zlib.git - cmds: - cd {{.THIRD_PARTY_DIR}}/zlib && git pull diff --git a/lib/Net.Compression/vnlib_compress/compression.c b/lib/Net.Compression/vnlib_compress/compression.c index ffe280b..a414609 100644 --- a/lib/Net.Compression/vnlib_compress/compression.c +++ b/lib/Net.Compression/vnlib_compress/compression.c @@ -130,22 +130,21 @@ VNLIB_EXPORT void* VNLIB_CC AllocateCompressor(CompressorType type, CompressionL switch (type) { + case COMP_TYPE_BROTLI: #ifdef VNLIB_COMPRESSOR_BROTLI_ENABLED - - case COMP_TYPE_BROTLI: result = BrAllocCompressor(state); - break; - #endif -#ifdef VNLIB_COMPRESSOR_ZLIB_ENABLED + break; case COMP_TYPE_DEFLATE: case COMP_TYPE_GZIP: + +#ifdef VNLIB_COMPRESSOR_ZLIB_ENABLED result = DeflateAllocCompressor(state); - break; #endif + break; /* * Unsupported compressor type allow error to propagate @@ -207,30 +206,25 @@ VNLIB_EXPORT int VNLIB_CC FreeCompressor(_In_ void* compressor) switch (comp->type) { - + case COMP_TYPE_BROTLI: #ifdef VNLIB_COMPRESSOR_BROTLI_ENABLED - - case COMP_TYPE_BROTLI: BrFreeCompressor(comp); - break; - #endif - -#ifdef VNLIB_COMPRESSOR_ZLIB_ENABLED + break; case COMP_TYPE_DEFLATE: - case COMP_TYPE_GZIP: - + case COMP_TYPE_GZIP: /* * Releasing a deflate compressor will cause a deflate * end call, which can fail, we should send the error * to the caller and clean up as best we can. */ - +#ifdef VNLIB_COMPRESSOR_ZLIB_ENABLED errorCode = DeflateFreeCompressor(comp); +#endif break; -#endif + /* * If compression type is none, there is nothing to do * since its not technically an error, so just return @@ -239,8 +233,7 @@ VNLIB_EXPORT int VNLIB_CC FreeCompressor(_In_ void* compressor) case COMP_TYPE_NONE: case COMP_TYPE_LZ4: default: - break; - + break; } /* @@ -267,26 +260,25 @@ VNLIB_EXPORT int64_t VNLIB_CC GetCompressedSize(_In_ const void* compressor, uin } comp = (CompressorState*)compressor; + result = ERR_COMP_TYPE_NOT_SUPPORTED; switch (comp->type) { -#ifdef VNLIB_COMPRESSOR_BROTLI_ENABLED - case COMP_TYPE_BROTLI: - result = BrGetCompressedSize(comp, inputLength); - break; +#ifdef VNLIB_COMPRESSOR_BROTLI_ENABLED + result = BrGetCompressedSize(comp, inputLength, flush); #endif - -#ifdef VNLIB_COMPRESSOR_ZLIB_ENABLED + break; case COMP_TYPE_DEFLATE: case COMP_TYPE_GZIP: - result = DeflateGetCompressedSize(comp, inputLength, flush); - break; +#ifdef VNLIB_COMPRESSOR_ZLIB_ENABLED + result = DeflateGetCompressedSize(comp, inputLength, flush); #endif + break; /* * Set the result as an error code, since the compressor @@ -294,8 +286,6 @@ VNLIB_EXPORT int64_t VNLIB_CC GetCompressedSize(_In_ const void* compressor, uin */ case COMP_TYPE_NONE: case COMP_TYPE_LZ4: - default: - result = ERR_COMP_TYPE_NOT_SUPPORTED; break; } @@ -350,31 +340,30 @@ VNLIB_EXPORT int VNLIB_CC CompressBlock(_In_ const void* compressor, Compression * compression function */ + result = ERR_COMP_TYPE_NOT_SUPPORTED; + switch (comp->type) { + /* Brolti support */ + case COMP_TYPE_BROTLI: - /* Brolti support */ #ifdef VNLIB_COMPRESSOR_BROTLI_ENABLED - - case COMP_TYPE_BROTLI: result = BrCompressBlock(comp, operation); - break; #endif + break; - /* Deflate support */ -#ifdef VNLIB_COMPRESSOR_ZLIB_ENABLED + /* Deflate support */ case COMP_TYPE_DEFLATE: case COMP_TYPE_GZIP: - result = DeflateCompressBlock(comp, operation); - break; +#ifdef VNLIB_COMPRESSOR_ZLIB_ENABLED + result = DeflateCompressBlock(comp, operation); #endif + break; case COMP_TYPE_NONE: case COMP_TYPE_LZ4: - default: - result = ERR_COMP_TYPE_NOT_SUPPORTED; break; } diff --git a/lib/Net.Compression/vnlib_compress/feature_brotli.c b/lib/Net.Compression/vnlib_compress/feature_brotli.c index 3a3e330..924f1af 100644 --- a/lib/Net.Compression/vnlib_compress/feature_brotli.c +++ b/lib/Net.Compression/vnlib_compress/feature_brotli.c @@ -220,8 +220,9 @@ int BrCompressBlock(const CompressorState* state, CompressionOperation* operatio } -int64_t BrGetCompressedSize(const CompressorState* state, uint64_t length) +int64_t BrGetCompressedSize(const CompressorState* state, uint64_t length, int32_t flush) { + (void)flush; /* * When the flush flag is set, the caller is requesting the * entire size of the compressed data, which can include metadata diff --git a/lib/Net.Compression/vnlib_compress/feature_brotli.h b/lib/Net.Compression/vnlib_compress/feature_brotli.h index b3cb35f..1f2090b 100644 --- a/lib/Net.Compression/vnlib_compress/feature_brotli.h +++ b/lib/Net.Compression/vnlib_compress/feature_brotli.h @@ -41,6 +41,6 @@ void BrFreeCompressor(CompressorState* state); int BrCompressBlock(const CompressorState* state, CompressionOperation* operation); -int64_t BrGetCompressedSize(const CompressorState* state, uint64_t length); +int64_t BrGetCompressedSize(const CompressorState* state, uint64_t length, int32_t flush); #endif /* !BROTLI_STUB_H_ */ \ No newline at end of file diff --git a/lib/Net.Compression/vnlib_compress/util.h b/lib/Net.Compression/vnlib_compress/util.h index 344a28b..8930da7 100644 --- a/lib/Net.Compression/vnlib_compress/util.h +++ b/lib/Net.Compression/vnlib_compress/util.h @@ -30,6 +30,14 @@ * Stub missing types and constants for GCC */ +/* +* If a custom allocator is enabled, use the native heap api +* header and assume linking is enabled +*/ +#ifdef VNLIB_CUSTOM_MALLOC_ENABLE +#include +#endif + #if defined(_MSC_VER) || defined(WIN32) || defined(_WIN32) #define IS_WINDOWS #endif @@ -100,56 +108,48 @@ #define ERR_INVALID_PTR -1 #define ERR_OUT_OF_MEMORY -2 -#if defined(VNLIB_CUSTOM_MALLOC_ENABLE) +#ifdef NATIVE_HEAP_API /* Defined in the NativeHeapApi */ + /* + * Add overrides for malloc, calloc, and free that use + * the nativeheap api to allocate memory + */ -/* -* Add rpmalloc overrides -*/ - -#include + static inline void* vnmalloc(size_t num, size_t size) + { + return heapAlloc(heapGetSharedHeapHandle(), num, size, FALSE); + } + static inline void* vncalloc(size_t num, size_t size) + { + return heapAlloc(heapGetSharedHeapHandle(), num, size, TRUE); + } -/* -* Add overrides for malloc, calloc, and free that use -* the nativeheap api to allocate memory -*/ + static inline void vnfree(void* ptr) + { + ERRNO result; + result = heapFree(heapGetSharedHeapHandle(), ptr); -static inline void* vnmalloc(size_t num, size_t size) -{ - return heapAlloc(heapGetSharedHeapHandle(), num, size, FALSE); -} - -static inline void* vncalloc(size_t num, size_t size) -{ - return heapAlloc(heapGetSharedHeapHandle(), num , size, TRUE); -} - -static inline void vnfree(void* ptr) -{ - ERRNO result; - result = heapFree(heapGetSharedHeapHandle(), ptr); - - //track failed free results - assert(result > 0); -} + //track failed free results + assert(result > 0); + } #else -/* -* Stub method for malloc. All calls to vnmalloc should be freed with vnfree. -*/ -#define vnmalloc(num, size) malloc(num * size) + /* + * Stub method for malloc. All calls to vnmalloc should be freed with vnfree. + */ + #define vnmalloc(num, size) malloc(num * size) -/* -* Stub method for free -*/ -#define vnfree(ptr) free(ptr) + /* + * Stub method for free + */ + #define vnfree(ptr) free(ptr) -/* -* Stub method for calloc. All calls to vncalloc should be freed with vnfree. -*/ -#define vncalloc(num, size) calloc(num, size) + /* + * Stub method for calloc. All calls to vncalloc should be freed with vnfree. + */ + #define vncalloc(num, size) calloc(num, size) -#endif +#endif // NATIVE_HEAP_API #endif /* !UTIL_H_ */ \ No newline at end of file diff --git a/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs b/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs index 827af56..90bdd8c 100644 --- a/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs +++ b/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs @@ -137,15 +137,13 @@ namespace VNLib.Net.Http.Core.Buffering } /// - public void ZeroAll() + public void FreeAll(bool zero) { - //Zero the buffer completely - MemoryUtil.InitializeBlock(_handle!.Memory); - } + if (zero) + { + MemoryUtil.InitializeBlock(_handle!.Memory); + } - /// - public void FreeAll() - { //Clear buffer memory structs to allow gc _requestHeaderBuffer.FreeBuffer(); _responseHeaderBuffer.FreeBuffer(); diff --git a/lib/Net.Http/src/Core/Buffering/IHttpBufferManager.cs b/lib/Net.Http/src/Core/Buffering/IHttpBufferManager.cs index c7020ff..9dc067a 100644 --- a/lib/Net.Http/src/Core/Buffering/IHttpBufferManager.cs +++ b/lib/Net.Http/src/Core/Buffering/IHttpBufferManager.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -78,14 +78,10 @@ namespace VNLib.Net.Http.Core.Buffering /// The pool to allocate memory from void AllocateBuffer(IHttpMemoryPool allocator); - /// - /// Zeros all internal buffers - /// - void ZeroAll(); - /// /// Frees all internal buffers /// - void FreeAll(); + /// A value that indicates if the buffer should be zeored before it's returned to the pool + void FreeAll(bool zeroAll); } } diff --git a/lib/Net.Http/src/Core/Compression/ManagedHttpCompressor.cs b/lib/Net.Http/src/Core/Compression/ManagedHttpCompressor.cs index fbb17c2..bfd848a 100644 --- a/lib/Net.Http/src/Core/Compression/ManagedHttpCompressor.cs +++ b/lib/Net.Http/src/Core/Compression/ManagedHttpCompressor.cs @@ -27,17 +27,8 @@ using System.Diagnostics; namespace VNLib.Net.Http.Core.Compression { - internal sealed class ManagedHttpCompressor : IResponseCompressor + internal sealed class ManagedHttpCompressor(IHttpCompressorManager manager) : IResponseCompressor { - //Store the compressor - private readonly IHttpCompressorManager _provider; - - public ManagedHttpCompressor(IHttpCompressorManager provider) - { - Debug.Assert(provider != null, "Expected non-null provider"); - _provider = provider; - } - /* * The compressor alloc is deferd until the first call to Init() * This is because user-code should not be called during the constructor @@ -59,7 +50,7 @@ namespace VNLib.Net.Http.Core.Compression Debug.Assert(!output.IsEmpty, "Expected non-zero output buffer"); //Compress the block - return _provider.CompressBlock(_compressor!, input, output); + return manager.CompressBlock(_compressor!, input, output); } /// @@ -69,19 +60,19 @@ namespace VNLib.Net.Http.Core.Compression Debug.Assert(_compressor != null); Debug.Assert(!output.IsEmpty, "Expected non-zero output buffer"); - return _provider.Flush(_compressor!, output); + return manager.Flush(_compressor!, output); } /// public void Init(CompressionMethod compMethod) { //Defer alloc the compressor - _compressor ??= _provider.AllocCompressor(); + _compressor ??= manager.AllocCompressor(); Debug.Assert(_compressor != null); //Init the compressor and get the block size - BlockSize = _provider.InitCompressor(_compressor, compMethod); + BlockSize = manager.InitCompressor(_compressor, compMethod); initialized = true; } @@ -94,7 +85,7 @@ namespace VNLib.Net.Http.Core.Compression { Debug.Assert(_compressor != null, "Compressor was initialized, exepcted a non null instance"); - _provider.DeinitCompressor(_compressor); + manager.DeinitCompressor(_compressor); initialized = false; } } diff --git a/lib/Net.Http/src/Core/HttpContext.cs b/lib/Net.Http/src/Core/HttpContext.cs index 3216a9b..8cecf30 100644 --- a/lib/Net.Http/src/Core/HttpContext.cs +++ b/lib/Net.Http/src/Core/HttpContext.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -25,6 +25,7 @@ using System; using System.IO; using System.Text; +using System.Diagnostics; using System.Threading.Tasks; using VNLib.Utils; @@ -82,7 +83,7 @@ namespace VNLib.Net.Http.Core private readonly ManagedHttpCompressor? _compressor; private ITransportContext? _ctx; - public HttpContext(HttpServer server) + public HttpContext(HttpServer server, CompressionMethod supportedMethods) { ParentServer = server; @@ -91,11 +92,17 @@ namespace VNLib.Net.Http.Core /* * We can alloc a new compressor if the server supports compression. * If no compression is supported, the compressor will never be accessed + * and never needs to be allocated */ - _compressor = server.SupportedCompressionMethods == CompressionMethod.None ? - null : - new ManagedHttpCompressor(server.Config.CompressorManager!); - + if (supportedMethods != CompressionMethod.None) + { + Debug.Assert(server.Config.CompressorManager != null, "Expected non-null provider"); + _compressor = new ManagedHttpCompressor(server.Config.CompressorManager); + } + else + { + _compressor = null; + } //Init buffer manager, if compression is supported, we need to alloc a buffer for the compressor Buffers = new(server.Config.BufferConfig, _compressor != null); @@ -190,12 +197,9 @@ namespace VNLib.Net.Http.Core //Release response/requqests Response.OnRelease(); - - //Zero before returning to pool - Buffers.ZeroAll(); - + //Free buffers - Buffers.FreeAll(); + Buffers.FreeAll(true); return true; } diff --git a/lib/Net.Http/src/Core/HttpServerBase.cs b/lib/Net.Http/src/Core/HttpServerBase.cs index a73867f..f5f3563 100644 --- a/lib/Net.Http/src/Core/HttpServerBase.cs +++ b/lib/Net.Http/src/Core/HttpServerBase.cs @@ -41,7 +41,6 @@ using System.Net.Sockets; using System.Threading.Tasks; using System.Collections.Frozen; using System.Collections.Generic; -using System.Security.Authentication; using VNLib.Utils.Logging; using VNLib.Utils.Memory.Caching; @@ -141,9 +140,7 @@ namespace VNLib.Net.Http //Configure roots and their directories ServerRoots = sites.ToFrozenDictionary(static r => r.Hostname, static tv => tv, StringComparer.OrdinalIgnoreCase); //Compile and store the timeout keepalive header - KeepAliveTimeoutHeaderValue = $"timeout={(int)_config.ConnectionKeepAlive.TotalSeconds}"; - //Create a new context store - ContextStore = ObjectRental.CreateReusable(() => new HttpContext(this)); + KeepAliveTimeoutHeaderValue = $"timeout={(int)_config.ConnectionKeepAlive.TotalSeconds}"; //Setup config copy with the internal http pool Transport = transport; //Cache supported compression methods, or none if compressor is null @@ -151,6 +148,9 @@ namespace VNLib.Net.Http CompressionMethod.None : config.CompressorManager.GetSupportedMethods(); + //Create a new context store + ContextStore = ObjectRental.CreateReusable(() => new HttpContext(this, SupportedCompressionMethods)); + //Cache wildcard root _wildcardRoot = ServerRoots.GetValueOrDefault(WILDCARD_KEY); @@ -269,20 +269,6 @@ namespace VNLib.Net.Http return Task.Run(ListenWorkerDoWork, cancellationToken); } - /* - * An SslStream may throw a win32 exception with HRESULT 0x80090327 - * when processing a client certificate (I believe anyway) only - * an issue on some clients (browsers) - */ - - private const int UKNOWN_CERT_AUTH_HRESULT = unchecked((int)0x80090327); - - /// - /// An invlaid frame size may happen if data is recieved on an open socket - /// but does not contain valid SSL handshake data - /// - private const int INVALID_FRAME_HRESULT = unchecked((int)0x80131620); - /* * A worker task that listens for connections from the transport */ @@ -301,7 +287,7 @@ namespace VNLib.Net.Http //Listen for new connection ITransportContext ctx = await Transport.AcceptAsync(StopToken!.Token); - //Try to dispatch the recieved event + //Try to dispatch the received event _ = DataReceivedAsync(ctx).ConfigureAwait(false); } catch (OperationCanceledException) @@ -309,14 +295,6 @@ namespace VNLib.Net.Http //Closing, exit loop break; } - catch(AuthenticationException ae) when(ae.HResult == INVALID_FRAME_HRESULT) - { - _config.ServerLog.Debug("A TLS connection attempt was made but an invalid TLS frame was received"); - } - catch (AuthenticationException ae) - { - _config.ServerLog.Error(ae); - } catch (Exception ex) { _config.ServerLog.Error(ex); diff --git a/lib/Net.Http/src/Core/Request/HttpInputStream.cs b/lib/Net.Http/src/Core/Request/HttpInputStream.cs index 7c010a3..e36d1e4 100644 --- a/lib/Net.Http/src/Core/Request/HttpInputStream.cs +++ b/lib/Net.Http/src/Core/Request/HttpInputStream.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -38,18 +38,14 @@ namespace VNLib.Net.Http.Core /// /// Specialized stream to allow reading a request entity body with a fixed content length. /// - internal sealed class HttpInputStream : Stream + internal sealed class HttpInputStream(IHttpContextInformation ContextInfo) : Stream { - private readonly IHttpContextInformation ContextInfo; - private long ContentLength; private Stream? InputStream; private long _position; - private InitDataBuffer? _initalData; - - public HttpInputStream(IHttpContextInformation contextInfo) => ContextInfo = contextInfo; + private InitDataBuffer? _initalData; private long Remaining => Math.Max(ContentLength - _position, 0); @@ -170,17 +166,16 @@ namespace VNLib.Net.Http.Core //Return number of bytes written to the buffer return writer.Written; - } - - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); - } + } public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { - //Calculate the amount of data that can be read into the buffer + /* + * Iniitally I'm calculating the amount of data that can be read into + * the buffer, up to the maxium input data size. This value will clamp + * the buffer in the writer below, so it cannot read more than is + * available from the transport. + */ int bytesToRead = (int)Math.Min(buffer.Length, Remaining); if (bytesToRead == 0) @@ -256,7 +251,7 @@ namespace VNLib.Net.Http.Core read = await ReadAsync(HttpServer.WriteOnlyScratchBuffer, CancellationToken.None) .ConfigureAwait(true); - } while (read != 0); + } while (read > 0); } /// diff --git a/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs b/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs index cacce4c..31493ba 100644 --- a/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs +++ b/lib/Net.Http/src/Core/Response/ChunkDataAccumulator.cs @@ -50,31 +50,14 @@ namespace VNLib.Net.Http.Core.Response * Must always leave enough room for trailing crlf at the end of * the buffer */ - private readonly int TotalMaxBufferSize => Buffer.Size - (int)Context.CrlfSegment.Length; - - /// - /// Complets and returns the memory segment containing the chunk data to send - /// to the client. This also resets the accumulator. - /// - /// - public readonly Memory GetChunkData(int accumulatedSize) - { - //Update the chunk size - int reservedOffset = UpdateChunkSize(Buffer, Context, accumulatedSize); - int endPtr = GetPointerToEndOfUsedBuffer(accumulatedSize); - - //Write trailing chunk delimiter - endPtr += Context.CrlfSegment.DangerousCopyTo(Buffer, endPtr); - - return Buffer.GetMemory()[reservedOffset..endPtr]; - } + private readonly int TotalMaxBufferSize => Buffer.Size - Context.CrlfSegment.Length; /// /// Complets and returns the memory segment containing the chunk data to send /// to the client. /// /// - public readonly Memory GetFinalChunkData(int accumulatedSize) + public readonly Memory GetChunkData(int accumulatedSize, bool isFinalChunk) { //Update the chunk size int reservedOffset = UpdateChunkSize(Buffer, Context, accumulatedSize); @@ -83,8 +66,11 @@ namespace VNLib.Net.Http.Core.Response //Write trailing chunk delimiter endPtr += Context.CrlfSegment.DangerousCopyTo(Buffer, endPtr); - //Write final chunk to the end of the accumulator - endPtr += Context.FinalChunkSegment.DangerousCopyTo(Buffer, endPtr); + if (isFinalChunk) + { + //Write final chunk to the end of the accumulator + endPtr += Context.FinalChunkSegment.DangerousCopyTo(Buffer, endPtr); + } return Buffer.GetMemory()[reservedOffset..endPtr]; } @@ -107,22 +93,6 @@ namespace VNLib.Net.Http.Core.Response => TotalMaxBufferSize - GetPointerToEndOfUsedBuffer(accumulatedSize); - /* - * Completed chunk is the segment of the buffer that contains the size segment - * followed by the accumulated chunk data, and the trailing crlf. - * - * The accumulated data position is the number of chunk bytes accumulated - * in the data segment. This does not include the number of reserved bytes - * are before it. - * - * We can get the value that points to the end of the used buffer - * and use the memory range operator to get the segment from the reserved - * segment, to the actual end of the data segment. - */ - private readonly Memory GetCompleteChunk(int reservedOffset, int accumulatedSize) - => Buffer.GetMemory()[reservedOffset..accumulatedSize]; - - private static int GetPointerToEndOfUsedBuffer(int accumulatedSize) => accumulatedSize + ReservedSize; /* @@ -181,14 +151,10 @@ namespace VNLib.Net.Http.Core.Response int reservedOffset = ReservedSize - totalChunkBufferBytes; //Copy encoded chunk size to the reserved segment - ref byte reservedSegRef = ref buffer.DangerousGetBinRef(reservedOffset); - ref byte chunkSizeBufRef = ref MemoryMarshal.GetReference(chunkSizeBinBuffer); - - //We know the block is super small MemoryUtil.SmallMemmove( - ref chunkSizeBufRef, + in MemoryMarshal.GetReference(chunkSizeBinBuffer), 0, - ref reservedSegRef, + ref buffer.DangerousGetBinRef(reservedOffset), 0, (ushort)totalChunkBufferBytes ); diff --git a/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs b/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs index 918ffe1..dcd0553 100644 --- a/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs +++ b/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs @@ -45,22 +45,14 @@ namespace VNLib.Net.Http.Core ValueTask discardTask = Request.InputStream.DiscardRemainingAsync(); - //See if response data needs to be written + //See if response data needs to be written, if so we can parallel discard and write if (ResponseBody.HasData) { //Parallel the write and discard Task response = WriteResponseInternalAsync(); - if (discardTask.IsCompletedSuccessfully) - { - //If discard is already complete, await the response - await response; - } - else - { - //If discard is not complete, await both, avoid wait-all method because it will allocate - await Task.WhenAll(discardTask.AsTask(), response); - } + //in .NET 8.0 WhenAll is now allocation free, so no biggie + await Task.WhenAll(discardTask.AsTask(), response); } else { diff --git a/lib/Net.Http/src/Core/Response/HttpResponse.cs b/lib/Net.Http/src/Core/Response/HttpResponse.cs index 3f2ae56..ec9879b 100644 --- a/lib/Net.Http/src/Core/Response/HttpResponse.cs +++ b/lib/Net.Http/src/Core/Response/HttpResponse.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Http @@ -39,16 +39,15 @@ using VNLib.Net.Http.Core.Buffering; namespace VNLib.Net.Http.Core.Response { - internal sealed class HttpResponse : IHttpLifeCycle + internal sealed class HttpResponse(IHttpContextInformation ContextInfo, IHttpBufferManager manager) : IHttpLifeCycle #if DEBUG , IStringSerializeable #endif { - private readonly IHttpContextInformation ContextInfo; - private readonly HashSet Cookies; - private readonly DirectStream ReusableDirectStream; - private readonly ChunkedStream ReusableChunkedStream; - private readonly HeaderDataAccumulator Writer; + private readonly HashSet Cookies = []; + private readonly DirectStream ReusableDirectStream = new(); + private readonly ChunkedStream ReusableChunkedStream = new(manager.ChunkAccumulatorBuffer, ContextInfo); + private readonly HeaderDataAccumulator Writer = new(manager.ResponseHeaderBuffer, ContextInfo); private int _headerWriterPosition; @@ -60,29 +59,13 @@ namespace VNLib.Net.Http.Core.Response /// /// Response header collection /// - public VnWebHeaderCollection Headers { get; } + public VnWebHeaderCollection Headers { get; } = []; /// /// The current http status code value /// internal HttpStatusCode StatusCode => _code; - public HttpResponse(IHttpContextInformation ctx, IHttpBufferManager manager) - { - ContextInfo = ctx; - - //Initialize a new header collection and a cookie jar - Headers = new(); - Cookies = new(); - - //Init header accumulator - Writer = new(manager.ResponseHeaderBuffer, ContextInfo); - - //Create a new chunked stream - ReusableChunkedStream = new(manager.ChunkAccumulatorBuffer, ContextInfo); - ReusableDirectStream = new(); - } - /// /// Sets the status code of the response /// @@ -365,9 +348,9 @@ namespace VNLib.Net.Http.Core.Response /// /// Writes chunked HTTP message bodies to an underlying streamwriter /// - private sealed class ChunkedStream : ReusableResponseStream, IResponseDataWriter + private sealed class ChunkedStream(IChunkAccumulatorBuffer buffer, IHttpContextInformation context) : ReusableResponseStream, IResponseDataWriter { - private readonly ChunkDataAccumulator _chunkAccumulator; + private readonly ChunkDataAccumulator _chunkAccumulator = new(buffer, context); /* * Tracks the number of bytes accumulated in the @@ -375,9 +358,6 @@ namespace VNLib.Net.Http.Core.Response */ private int _accumulatedBytes; - public ChunkedStream(IChunkAccumulatorBuffer buffer, IHttpContextInformation context) - => _chunkAccumulator = new(buffer, context); - #region Hooks /// @@ -402,11 +382,9 @@ namespace VNLib.Net.Http.Core.Response * write the final termination sequence to the transport. */ - Memory chunkData = isFinal ? - _chunkAccumulator.GetFinalChunkData(_accumulatedBytes) : - _chunkAccumulator.GetChunkData(_accumulatedBytes); + Memory chunkData = _chunkAccumulator.GetChunkData(_accumulatedBytes, isFinal); - //Reset accumulator + //Reset accumulator now that we captured the final chunk _accumulatedBytes = 0; //Write remaining data to stream diff --git a/lib/Net.Http/src/Core/Response/HttpStreamResponse.cs b/lib/Net.Http/src/Core/Response/HttpStreamResponse.cs new file mode 100644 index 0000000..b08d2ab --- /dev/null +++ b/lib/Net.Http/src/Core/Response/HttpStreamResponse.cs @@ -0,0 +1,50 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpStreamResponse.cs +* +* HttpStreamResponse.cs is part of VNLib.Net.Http which is part +* of the larger VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +/* + * This file handles response entity processing. It handles in-memory response + * processing, as well as stream response processing. It handles constraints + * such as content-range limits. I tried to eliminate or reduce the amount of + * memory copying required to process the response entity. + */ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Net.Http.Core.Response +{ + internal readonly struct HttpStreamResponse(Stream stream) : IHttpStreamResponse + { + /// + public readonly void Dispose() => stream.Dispose(); + + /// + public readonly ValueTask DisposeAsync() => stream.DisposeAsync(); + + /// + public readonly ValueTask ReadAsync(Memory buffer) => stream!.ReadAsync(buffer, CancellationToken.None); + } +} \ No newline at end of file diff --git a/lib/Net.Http/src/Core/Response/HttpstreamResponse.cs b/lib/Net.Http/src/Core/Response/HttpstreamResponse.cs new file mode 100644 index 0000000..b08d2ab --- /dev/null +++ b/lib/Net.Http/src/Core/Response/HttpstreamResponse.cs @@ -0,0 +1,50 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpStreamResponse.cs +* +* HttpStreamResponse.cs is part of VNLib.Net.Http which is part +* of the larger VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +/* + * This file handles response entity processing. It handles in-memory response + * processing, as well as stream response processing. It handles constraints + * such as content-range limits. I tried to eliminate or reduce the amount of + * memory copying required to process the response entity. + */ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Net.Http.Core.Response +{ + internal readonly struct HttpStreamResponse(Stream stream) : IHttpStreamResponse + { + /// + public readonly void Dispose() => stream.Dispose(); + + /// + public readonly ValueTask DisposeAsync() => stream.DisposeAsync(); + + /// + public readonly ValueTask ReadAsync(Memory buffer) => stream!.ReadAsync(buffer, CancellationToken.None); + } +} \ No newline at end of file diff --git a/lib/Net.Http/src/Core/Response/ResponsBodyDataState.cs b/lib/Net.Http/src/Core/Response/ResponsBodyDataState.cs new file mode 100644 index 0000000..797d490 --- /dev/null +++ b/lib/Net.Http/src/Core/Response/ResponsBodyDataState.cs @@ -0,0 +1,100 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ResponseWriter.cs +* +* ResponseWriter.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +/* + * This file handles response entity processing. It handles in-memory response + * processing, as well as stream response processing. It handles constraints + * such as content-range limits. I tried to eliminate or reduce the amount of + * memory copying required to process the response entity. + */ + +using System.IO; + +namespace VNLib.Net.Http.Core.Response +{ + internal readonly struct ResponsBodyDataState + { + /// + /// A value that inidcates if the response entity has been set + /// + public readonly bool IsSet; + /// + /// A value that indicates if the response entity requires buffering + /// + public readonly bool BufferRequired; + /// + /// The length (in bytes) of the response entity + /// + public readonly long Legnth; + + public readonly IHttpStreamResponse? Stream; + public readonly IMemoryResponseReader? MemResponse; + public readonly Stream? RawStream; + + private ResponsBodyDataState(IHttpStreamResponse stream, long length) + { + Legnth = length; + Stream = stream; + MemResponse = null; + RawStream = null; + IsSet = true; + BufferRequired = true; + } + + private ResponsBodyDataState(IMemoryResponseReader reader) + { + Legnth = reader.Remaining; + MemResponse = reader; + Stream = null; + RawStream = null; + IsSet = true; + BufferRequired = false; + } + + private ResponsBodyDataState(Stream stream, long length) + { + Legnth = length; + Stream = null; + MemResponse = null; + RawStream = stream; + IsSet = true; + BufferRequired = true; + } + + internal readonly HttpStreamResponse GetRawStreamResponse() => new(RawStream!); + + internal readonly void Dispose() + { + Stream?.Dispose(); + MemResponse?.Close(); + RawStream?.Dispose(); + } + + public static ResponsBodyDataState FromMemory(IMemoryResponseReader stream) => new(stream); + + public static ResponsBodyDataState FromStream(IHttpStreamResponse stream, long length) => new(stream, length); + + public static ResponsBodyDataState FromRawStream(Stream stream, long length) => new(stream, length); + } +} \ No newline at end of file diff --git a/lib/Net.Http/src/Core/Response/ResponseWriter.cs b/lib/Net.Http/src/Core/Response/ResponseWriter.cs index b5a167c..b60537d 100644 --- a/lib/Net.Http/src/Core/Response/ResponseWriter.cs +++ b/lib/Net.Http/src/Core/Response/ResponseWriter.cs @@ -32,7 +32,6 @@ using System; using System.IO; using System.Diagnostics; -using System.Threading; using System.Threading.Tasks; using System.Runtime.CompilerServices; @@ -41,15 +40,13 @@ using VNLib.Net.Http.Core.Compression; namespace VNLib.Net.Http.Core.Response { - internal sealed class ResponseWriter : IHttpResponseBody { private ResponsBodyDataState _userState; /// public bool HasData => _userState.IsSet; - - //Buffering is required when a stream is set + /// public bool BufferRequired => _userState.BufferRequired; @@ -72,7 +69,7 @@ namespace VNLib.Net.Http.Core.Response Debug.Assert(response != null, "Stream value is null, illegal operation"); Debug.Assert(length > -1, "explicit length passed a negative value, illegal operation"); - _userState = new(response, length); + _userState = ResponsBodyDataState.FromStream(response, length); return true; } @@ -91,7 +88,7 @@ namespace VNLib.Net.Http.Core.Response Debug.Assert(response != null, "Memory response argument was null and expected a value"); //Assign user-state - _userState = new(response); + _userState = ResponsBodyDataState.FromMemory(response); return true; } @@ -112,147 +109,144 @@ namespace VNLib.Net.Http.Core.Response Debug.Assert(length > -1, "explicit length passed a negative value, illegal operation"); //Assign user-state - _userState = new(rawStream, length); + _userState = ResponsBodyDataState.FromRawStream(rawStream, length); return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnComplete() + { + //Clear response containers + _userState.Dispose(); + _userState = default; + + _readSegment = default; + } + + private ReadOnlyMemory _readSegment; #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task /// - public async Task WriteEntityAsync(IDirectResponsWriter dest, Memory buffer) + public Task WriteEntityAsync(IDirectResponsWriter dest, Memory buffer) => WriteEntityAsync(dest, buffer, 0); + + /// + public async Task WriteEntityAsync(TComp compressor, IResponseDataWriter writer, Memory buffer) + where TComp : IResponseCompressor { - //Write a sliding window response - if (_userState.MemResponse != null) - { - //Write response body from memory - while (_userState.MemResponse.Remaining > 0) - { - //Get remaining segment - _readSegment = _userState.MemResponse.GetMemory(); + //Create a chunked response writer struct to pass to write async function + ChunkedResponseWriter output = new(writer, compressor); - //Write segment to output stream - await dest.WriteAsync(_readSegment); + await WriteEntityAsync(output, buffer, compressor.BlockSize); - //Advance by the written amount - _userState.MemResponse.Advance(_readSegment.Length); - } + /* + * Once there is no more response data avialable to compress + * we need to flush the compressor, then flush the writer + * to publish all accumulated data to the client + */ - //Disposing of memory response can be deferred until the end of the request since its always syncrhonous - } - else if(_userState.RawStream != null) + do { - Debug.Assert(!buffer.IsEmpty, "Transfer buffer is required for streaming operations"); + //Flush the compressor output + int written = compressor.Flush(writer.GetMemory()); - await ProcessStreamDataAsync(_userState.GetRawStreamResponse(), dest, buffer); - } - else - { - Debug.Assert(!buffer.IsEmpty, "Transfer buffer is required for streaming operations"); + //No more data to buffer + if (written == 0) + { + //final flush and exit + await writer.FlushAsync(true); + break; + } - Debug.Assert(_userState.Stream != null, "Stream value is null, illegal operation"); + if (writer.Advance(written) == 0) + { + //Flush because accumulator is full + await writer.FlushAsync(false); + } - await ProcessStreamDataAsync(_userState.Stream!, dest, buffer); - } + } while (true); } - /// - public async Task WriteEntityAsync(TComp comp, IResponseDataWriter writer, Memory buffer) where TComp : IResponseCompressor + private async Task WriteEntityAsync(TResWriter dest, Memory buffer, int blockSize) + where TResWriter : IDirectResponsWriter { //try to clamp the buffer size to the compressor block size - int maxBufferSize = Math.Min(buffer.Length, comp.BlockSize); - if(maxBufferSize > 0) + if (blockSize > 0) { - buffer = buffer[..maxBufferSize]; + buffer = buffer[..Math.Min(buffer.Length, blockSize)]; } - ChunkedResponseWriter output = new(writer, comp); - //Write a sliding window response if (_userState.MemResponse != null) { - while (_userState.MemResponse.Remaining > 0) + if (blockSize > 0) + { + while (_userState.MemResponse.Remaining > 0) + { + //Get next segment clamped to the block size + _readSegment = _userState.MemResponse.GetRemainingConstrained(blockSize); + + //Commit output bytes + await dest.WriteAsync(_readSegment); + + //Advance by the written amount + _userState.MemResponse.Advance(_readSegment.Length); + } + } + else { - //Get next segment - _readSegment = comp.BlockSize > 0 ? - _userState.MemResponse.GetRemainingConstrained(comp.BlockSize) - : _userState.MemResponse.GetMemory(); + //Write response body from memory + while (_userState.MemResponse.Remaining > 0) + { + //Get remaining segment + _readSegment = _userState.MemResponse.GetMemory(); - //Commit output bytes - await output.WriteAsync(_readSegment); + //Write segment to output stream + await dest.WriteAsync(_readSegment); - //Advance by the written amount - _userState.MemResponse.Advance(_readSegment.Length); + //Advance by the written amount + _userState.MemResponse.Advance(_readSegment.Length); + } } //Disposing of memory response can be deferred until the end of the request since its always syncrhonous } - else if(_userState.RawStream != null) + else if (_userState.RawStream != null) { Debug.Assert(!buffer.IsEmpty, "Transfer buffer is required for streaming operations"); - //Configure a raw stream response - await ProcessStreamDataAsync(_userState.GetRawStreamResponse(), output, buffer); + await ProcessStreamDataAsync(_userState.GetRawStreamResponse(), dest, buffer, _userState.Legnth); } else { Debug.Assert(!buffer.IsEmpty, "Transfer buffer is required for streaming operations"); + Debug.Assert(_userState.Stream != null, "Stream value is null, illegal state"); - Debug.Assert(_userState.Stream != null, "Stream value is null, illegal operation"); - await ProcessStreamDataAsync(_userState.Stream, output, buffer); + await ProcessStreamDataAsync(_userState.Stream, dest, buffer, _userState.Legnth); } - - - /* - * Once there is no more response data avialable to compress - * we need to flush the compressor, then flush the writer - * to publish all accumulated data to the client - */ - - do - { - //Flush the compressor output - int written = comp.Flush(writer.GetMemory()); - - //No more data to buffer - if (written == 0) - { - //final flush and exit - await writer.FlushAsync(true); - break; - } - - if (writer.Advance(written) == 0) - { - //Flush because accumulator is full - await writer.FlushAsync(false); - } - - } while (true); } - private async Task ProcessStreamDataAsync(TStream stream, TWriter dest, Memory buffer) - where TStream: IHttpStreamResponse - where TWriter: IDirectResponsWriter + private static async Task ProcessStreamDataAsync(TStream stream, TWriter dest, Memory buffer, long length) + where TStream : IHttpStreamResponse + where TWriter : IDirectResponsWriter { /* * When streams are used, callers will submit an explict length value * which must be respected. This allows the stream size to differ from * the actual content length. This is useful for when the stream is - * non-seekable, or does not have a known length + * non-seekable, or does not have a known length. Also used for + * content-range responses, that are shorter than the whole stream. */ - long total = 0; - while (total < Length) + long sentBytes = 0; + do { - //get offset wrapper of the total buffer or remaining count - Memory offset = buffer[..(int)Math.Min(buffer.Length, Length - total)]; + Memory offset = ClampCopyBuffer(buffer, length, sentBytes); - //read + //read only the amount of data that is required int read = await stream.ReadAsync(offset); - //Guard if (read == 0) { break; @@ -261,110 +255,31 @@ namespace VNLib.Net.Http.Core.Response //write only the data that was read (slice) await dest.WriteAsync(offset[..read]); - //Update total - total += read; - } + sentBytes += read; + + } while (sentBytes < length); //Try to dispose the response stream asyncrhonously since we are done with it await stream.DisposeAsync(); } - - private static bool CompressNextSegment(ref ForwardOnlyMemoryReader reader, TComp comp, IResponseDataWriter writer) - where TComp: IResponseCompressor + + private static Memory ClampCopyBuffer(Memory buffer, long contentLength, long sentBytes) { - //Get output buffer - Memory output = writer.GetMemory(); - - //Compress the trimmed block - CompressionResult res = comp.CompressBlock(reader.Window, output); - ValidateCompressionResult(in res); - - //Commit input bytes - reader.Advance(res.BytesRead); - - return writer.Advance(res.BytesWritten) == 0; + //get offset wrapper of the total buffer or remaining count + int bufferSize = (int)Math.Min(buffer.Length, contentLength - sentBytes); + return buffer[..bufferSize]; } [Conditional("DEBUG")] - private static void ValidateCompressionResult(in CompressionResult result) - { + private static void ValidateCompressionResult(in CompressionResult result, int segmentLen) + { Debug.Assert(result.BytesRead > -1, "Compression result returned a negative bytes read value"); Debug.Assert(result.BytesWritten > -1, "Compression result returned a negative bytes written value"); + Debug.Assert(result.BytesWritten <= segmentLen, "Compression result wrote more bytes than the input segment length"); } #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void OnComplete() - { - //Clear rseponse containers - _userState.Dispose(); - _userState = default; - - _readSegment = default; - } - - - private readonly struct ResponsBodyDataState - { - public readonly bool IsSet; - public readonly bool BufferRequired; - public readonly long Legnth; - public readonly IHttpStreamResponse? Stream; - public readonly IMemoryResponseReader? MemResponse; - public readonly Stream? RawStream; - - public ResponsBodyDataState(IHttpStreamResponse stream, long length) - { - Legnth = length; - Stream = stream; - MemResponse = null; - RawStream = null; - IsSet = true; - BufferRequired = true; - } - - public ResponsBodyDataState(IMemoryResponseReader reader) - { - Legnth = reader.Remaining; - MemResponse = reader; - Stream = null; - RawStream = null; - IsSet = true; - BufferRequired = false; - } - - public ResponsBodyDataState(Stream stream, long length) - { - Legnth = length; - Stream = null; - MemResponse = null; - RawStream = stream; - IsSet = true; - BufferRequired = true; - } - - public readonly HttpstreamResponse GetRawStreamResponse() => new(RawStream!); - - public readonly void Dispose() - { - Stream?.Dispose(); - MemResponse?.Close(); - RawStream?.Dispose(); - } - } - - private readonly struct HttpstreamResponse(Stream stream) : IHttpStreamResponse - { - /// - public readonly void Dispose() => stream.Dispose(); - - /// - public readonly ValueTask DisposeAsync() => stream.DisposeAsync(); - - /// - public readonly ValueTask ReadAsync(Memory buffer) => stream!.ReadAsync(buffer, CancellationToken.None); - } + private readonly struct ChunkedResponseWriter(IResponseDataWriter writer, TComp comp) : IDirectResponsWriter where TComp : IResponseCompressor @@ -378,7 +293,7 @@ namespace VNLib.Net.Http.Core.Response do { //Compress the buffered data and flush if required - if (CompressNextSegment(ref streamReader, comp, writer)) + if (CompressNextSegment(ref streamReader)) { //Time to flush await writer.FlushAsync(false); @@ -386,6 +301,21 @@ namespace VNLib.Net.Http.Core.Response } while (streamReader.WindowSize > 0); } + + private readonly bool CompressNextSegment(ref ForwardOnlyMemoryReader reader) + { + //Get output buffer + Memory output = writer.GetMemory(); + + //Compress the trimmed block + CompressionResult res = comp.CompressBlock(reader.Window, output); + ValidateCompressionResult(in res, output.Length); + + //Commit input bytes + reader.Advance(res.BytesRead); + + return writer.Advance(res.BytesWritten) == 0; + } } } } \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs b/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs index e346af1..2f2ba91 100644 --- a/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs +++ b/lib/Plugins.Essentials/src/Accounts/UserCreationRequest.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -22,6 +22,8 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; + using VNLib.Utils.Memory; using VNLib.Plugins.Essentials.Users; @@ -40,7 +42,17 @@ namespace VNLib.Plugins.Essentials.Accounts public ulong Privileges { get; init; } = AccountUtil.MINIMUM_LEVEL; /// - public string EmailAddress { get; init; } = string.Empty; + public string Username { get; init; } = string.Empty; + + /// + /// Obsolete: Use the Username property instead + /// + [Obsolete("Use the Username property instead")] + public string EmailAddress + { + get => Username; + init => Username = value; + } /// public bool UseRawPassword { get; init; } diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs index 61a9b64..f052c56 100644 --- a/lib/Plugins.Essentials/src/EventProcessor.cs +++ b/lib/Plugins.Essentials/src/EventProcessor.cs @@ -188,8 +188,7 @@ namespace VNLib.Plugins.Essentials //If the processor had an error recovering the session, return the result to the processor if (entity.EventSessionHandle.EntityStatus != FileProcessArgs.Continue) { - ProcessFile(httpEvent, entity.EventSessionHandle.EntityStatus); - return; + goto ProcessRoutine; } //Attach the new session to the entity @@ -209,7 +208,7 @@ namespace VNLib.Plugins.Essentials //Loop through nodes while(mwNode != null) { - //Process + //Invoke mw handler on our event entity.EventArgs = await mwNode.ValueRef.ProcessAsync(entity); switch (entity.EventArgs.Routine) @@ -237,7 +236,7 @@ namespace VNLib.Plugins.Essentials //Process a virtual file GetArgsFromVirtualReturn(entity, rt, out entity.EventArgs); - //If the entity was processed, exit + //If the entity was processed by the handler, exit if (entity.EventArgs != FileProcessArgs.Continue) { goto RespondAndExit; @@ -275,8 +274,10 @@ namespace VNLib.Plugins.Essentials } } + ProcessRoutine: + //Finally process the file - ProcessFile(httpEvent, in entity.EventArgs); + ProcessRoutine(httpEvent, in entity.EventArgs); } catch (ContentTypeUnacceptableException) { @@ -329,7 +330,7 @@ namespace VNLib.Plugins.Essentials /// /// The entity to process the file for /// The selected to determine what file to process - protected virtual void ProcessFile(IHttpEvent entity, ref readonly FileProcessArgs args) + protected virtual void ProcessRoutine(IHttpEvent entity, ref readonly FileProcessArgs args) { try { @@ -413,8 +414,7 @@ namespace VNLib.Plugins.Essentials //Get the content type of he file ContentType fileType = HttpHelpers.GetContentTypeFromFile(filename); - - //Make sure the client accepts the content type + if (!entity.Server.Accepts(fileType)) { //Unacceptable @@ -729,7 +729,7 @@ namespace VNLib.Plugins.Essentials WeakReference wr = Volatile.Read(ref _objects[tableIndex]); //Try to get the object instance - wr.TryGetTarget(out object? value); + _ = wr.TryGetTarget(out object? value); instance = (T?)value; } diff --git a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs index 92fae08..a99b1ab 100644 --- a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs @@ -354,6 +354,7 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// /// + /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs index f49af32..5f59346 100644 --- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs @@ -163,6 +163,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponseJson(this IHttpEvent ev, HttpStatusCode code, JsonDocument data) { + ArgumentNullException.ThrowIfNull(ev); + if(data == null) { ev.CloseResponse(code); @@ -195,7 +197,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, T webm) where T:WebMessage { - if (webm == null) + ArgumentNullException.ThrowIfNull(ev); + + if (webm is null) { ev.CloseResponse(HttpStatusCode.OK); } @@ -220,6 +224,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, FileInfo file) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(file); + //Close with file ev.CloseResponse(code, file); //Set content dispostion as attachment (only if successfull) @@ -236,6 +243,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, FileStream file) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(file); + //Close with file ev.CloseResponse(code, file); //Set content dispostion as attachment (only if successfull) @@ -255,6 +265,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponseAttachment(this IHttpEvent ev, HttpStatusCode code, ContentType ct, Stream data, string fileName, long length) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(data); + //Close with file ev.CloseResponse(code, ct, data, length); //Set content dispostion as attachment (only if successfull) @@ -275,6 +288,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, FileInfo file) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(file); + //Open filestream for file FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read); try @@ -302,13 +318,16 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, FileStream file) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(file); + //Get content type from filename ContentType ct = HttpHelpers.GetContentTypeFromFile(file.Name); //Set the input as a stream ev.CloseResponse(code, ct, file, file.Length); } - + /// /// Close a response to a connection with a character buffer using the server wide /// encoding @@ -321,12 +340,10 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data) - { + public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data) => //Get a memory stream using server built-in encoding CloseResponse(ev, code, type, data, ev.Server.Encoding); - } - + /// /// Close a response to a connection with a character buffer using the specified encoding type /// @@ -339,6 +356,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data, Encoding encoding) { + ArgumentNullException.ThrowIfNull(ev); + if (data.IsEmpty) { ev.CloseResponse(code); @@ -367,6 +386,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data) { + ArgumentNullException.ThrowIfNull(ev); + if (data.IsEmpty) { ev.CloseResponse(code); @@ -391,6 +412,9 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool CloseWithRelativeFile(this HttpEntity entity, HttpStatusCode code, string filePath) { + ArgumentNullException.ThrowIfNull(entity); + ArgumentNullException.ThrowIfNull(filePath); + //See if file exists and is within the root's directory if (entity.RequestedRoot.FindResourceInRoot(filePath, out string realPath)) { @@ -412,11 +436,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// Sets required headers for redirection, disables cache control, and returns the status code to the client /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Redirect(this IHttpEvent ev, RedirectType type, string location) - { - Redirect(ev, type, new Uri(location, UriKind.RelativeOrAbsolute)); - } - + public static void Redirect(this IHttpEvent ev, RedirectType type, string location) + => Redirect(ev, type, new Uri(location, UriKind.RelativeOrAbsolute)); + /// /// Redirects a client using the specified /// @@ -427,6 +449,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Redirect(this IHttpEvent ev, RedirectType type, Uri location) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(location); if(type == RedirectType.None) { @@ -472,6 +496,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryGetJsonFromArg(this IHttpEvent ev, string key, JsonSerializerOptions options, out T? obj) { + ArgumentNullException.ThrowIfNull(ev); + //Check for key in argument if (ev.RequestArgs.TryGetNonEmptyValue(key, out string? value)) { @@ -502,6 +528,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static JsonDocument? GetJsonFromArg(this IHttpEvent ev, string key, in JsonDocumentOptions options = default) { + ArgumentNullException.ThrowIfNull(ev); + try { //Check for key in argument @@ -528,6 +556,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? GetJsonFromFile(this IHttpEvent ev, JsonSerializerOptions? options = null, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + if (ev.Files.Count <= uploadIndex) { return default; @@ -563,6 +593,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static JsonDocument? GetJsonFromFile(this IHttpEvent ev, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + if (ev.Files.Count <= uploadIndex) { return default; @@ -599,6 +631,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ValueTask GetJsonFromFileAsync(this HttpEntity ev, JsonSerializerOptions? options = null, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + if (ev.Files.Count <= uploadIndex) { return ValueTask.FromResult(default); @@ -640,6 +674,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Task GetJsonFromFileAsync(this HttpEntity ev, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + if (ev.Files.Count <= uploadIndex) { return DocTaskDefault; @@ -677,6 +713,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// public static Task ParseFileAsAsync(this IHttpEvent ev, Func> parser, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(parser); + if (ev.Files.Count <= uploadIndex) { return Task.FromResult(default); @@ -698,6 +737,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// public static Task ParseFileAsAsync(this IHttpEvent ev, Func> parser, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(parser); + if (ev.Files.Count <= uploadIndex) { return Task.FromResult(default); @@ -720,6 +762,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// public static ValueTask ParseFileAsAsync(this IHttpEvent ev, Func> parser, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(parser); + if (ev.Files.Count <= uploadIndex) { return ValueTask.FromResult(default); @@ -741,6 +786,9 @@ namespace VNLib.Plugins.Essentials.Extensions /// public static ValueTask ParseFileAsAsync(this IHttpEvent ev, Func> parser, int uploadIndex = 0) { + ArgumentNullException.ThrowIfNull(ev); + ArgumentNullException.ThrowIfNull(parser); + if (ev.Files.Count <= uploadIndex) { return ValueTask.FromResult(default); @@ -760,6 +808,8 @@ namespace VNLib.Plugins.Essentials.Extensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool HasAuthorization(this IConnectionInfo ci, [NotNullWhen(true)] out string? token) { + ArgumentNullException.ThrowIfNull(ci); + //Get auth header value string? authorization = ci.Headers[HttpRequestHeader.Authorization]; //Check if its set @@ -767,7 +817,7 @@ namespace VNLib.Plugins.Essentials.Extensions { int bearerIndex = authorization.IndexOf(BEARER_STRING, StringComparison.OrdinalIgnoreCase); //Calc token offset, get token, and trim any whitespace - token = authorization[(bearerIndex + BEARER_LEN)..].Trim(); + token = authorization.AsSpan(bearerIndex + BEARER_LEN).Trim().ToString(); return true; } token = null; @@ -819,11 +869,9 @@ namespace VNLib.Plugins.Essentials.Extensions ) { //Must define an accept callback - _ = socketOpenedCallback ?? throw new ArgumentNullException(nameof(socketOpenedCallback)); - - bool success = PrepWebSocket(entity, subProtocol); + ArgumentNullException.ThrowIfNull(socketOpenedCallback); - if (success) + if (PrepWebSocket(entity, subProtocol)) { //Set a default keep alive if none was specified if (keepAlive == default) @@ -841,8 +889,11 @@ namespace VNLib.Plugins.Essentials.Extensions //Setup a new websocket session with a new session id entity.DangerousChangeProtocol(ws); + + return true; } - return success; + + return false; } /// @@ -858,11 +909,10 @@ namespace VNLib.Plugins.Essentials.Extensions public static bool AcceptWebSocket(this IHttpEvent entity, WebSocketAcceptedCallback socketOpenedCallback, string? subProtocol = null, TimeSpan keepAlive = default) { //Must define an accept callback - _ = socketOpenedCallback ?? throw new ArgumentNullException(nameof(socketOpenedCallback)); + ArgumentNullException.ThrowIfNull(entity); + ArgumentNullException.ThrowIfNull(socketOpenedCallback); - bool success = PrepWebSocket(entity, subProtocol); - - if(success) + if(PrepWebSocket(entity, subProtocol)) { //Set a default keep alive if none was specified if (keepAlive == default) @@ -879,15 +929,19 @@ namespace VNLib.Plugins.Essentials.Extensions //Setup a new websocket session with a new session id entity.DangerousChangeProtocol(ws); + + return true; } - return success; + return false; } private static string GetNewSocketId() => Guid.NewGuid().ToString("N"); private static bool PrepWebSocket(this IHttpEvent entity, string? subProtocol = null) { + ArgumentNullException.ThrowIfNull(entity); + //Make sure this is a websocket request if (!entity.Server.IsWebSocketRequest) { diff --git a/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs b/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs index a235b13..e1706f4 100644 --- a/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs +++ b/lib/Plugins.Essentials/src/SemiConsistentVeTable.cs @@ -28,6 +28,7 @@ using System.Threading; using System.Threading.Tasks; using System.Collections.Frozen; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using VNLib.Net.Http; using VNLib.Plugins.Essentials.Endpoints; @@ -65,6 +66,7 @@ namespace VNLib.Plugins.Essentials new Dictionary>(StringComparer.OrdinalIgnoreCase) .ToFrozenDictionary(); + private bool _isEmpty = true; /* * A lock that is held by callers that intend to @@ -73,14 +75,14 @@ namespace VNLib.Plugins.Essentials private readonly object VeUpdateLock = new(); /// - public bool IsEmpty => VirtualEndpoints.Count == 0; + public bool IsEmpty => _isEmpty; /// public void AddEndpoint(params IEndpoint[] endpoints) { //Check - _ = endpoints ?? throw new ArgumentNullException(nameof(endpoints)); + ArgumentNullException.ThrowIfNull(endpoints); //Make sure all endpoints specify a path if (endpoints.Any(static e => string.IsNullOrWhiteSpace(e?.Path))) { @@ -105,6 +107,7 @@ namespace VNLib.Plugins.Essentials //Uinion endpoints by their paths to combine them IEnumerable> allEndpoints = eps.UnionBy(evs, static s => s.Path); + //Only allow 1 thread at a time to mutate the table lock (VeUpdateLock) { //Clone the current dictonary @@ -115,10 +118,11 @@ namespace VNLib.Plugins.Essentials newTable.Add(ep.Path, ep); } - FrozenDictionary> newTableFrozen = newTable.ToFrozenDictionary(); + //Update is-empty flag + _isEmpty = newTable.Count == 0; - //Store the new table - _ = Interlocked.Exchange(ref VirtualEndpoints, newTableFrozen); + //Create the new table and store the entire table + _ = Interlocked.Exchange(ref VirtualEndpoints, newTable.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase)); } } @@ -157,24 +161,27 @@ namespace VNLib.Plugins.Essentials _ = newTable.Remove(eps); } - FrozenDictionary> newTableFrozen = newTable.ToFrozenDictionary(); + //Update is-empty flag + _isEmpty = newTable.Count == 0; //Store the new table - _ = Interlocked.Exchange(ref VirtualEndpoints, newTableFrozen); + _ = Interlocked.Exchange(ref VirtualEndpoints, newTable.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase)); } } /// - public bool TryGetEndpoint(string path, out IVirtualEndpoint? endpoint) => VirtualEndpoints.TryGetValue(path, out endpoint); + public bool TryGetEndpoint(string path, [NotNullWhen(true)] out IVirtualEndpoint? endpoint) + => VirtualEndpoints.TryGetValue(path, out endpoint); /* * Wrapper class for converting IHttpEvent endpoints to * httpEntityEndpoints */ - private sealed record class EvEndpointWrapper(IVirtualEndpoint Wrapped) : IVirtualEndpoint + private sealed class EvEndpointWrapper(IVirtualEndpoint Wrapped) : IVirtualEndpoint { string IEndpoint.Path => Wrapped.Path; + ValueTask IVirtualEndpoint.Process(HttpEntity entity) => Wrapped.Process(entity); } } diff --git a/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs b/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs index d3fc475..05d6712 100644 --- a/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs +++ b/lib/Plugins.Essentials/src/Sessions/ISessionExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -49,7 +49,7 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetOrigin(this ISession session) => session[ORIGIN_ENTRY]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Uri GetOriginUri(this ISession session) => Uri.TryCreate(session[ORIGIN_ENTRY], UriKind.Absolute, out Uri origin) ? origin : null; + public static Uri? GetOriginUri(this ISession session) => Uri.TryCreate(session[ORIGIN_ENTRY], UriKind.Absolute, out Uri? origin) ? origin : null; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetOrigin(this ISession session, string origin) => session[ORIGIN_ENTRY] = origin; diff --git a/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs b/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs index a5b9a30..bd89be1 100644 --- a/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs +++ b/lib/Plugins.Essentials/src/Users/IUserCreationRequest.cs @@ -44,9 +44,9 @@ namespace VNLib.Plugins.Essentials.Users ulong Privileges { get; } /// - /// The user's email address + /// The user's unique username (may also be an email address /// - string EmailAddress { get; } + string Username { get; } /// /// Should the password be stored as-is in the database? diff --git a/lib/Utils/src/IO/VnMemoryStream.cs b/lib/Utils/src/IO/VnMemoryStream.cs index ed8ed5a..ada2c64 100644 --- a/lib/Utils/src/IO/VnMemoryStream.cs +++ b/lib/Utils/src/IO/VnMemoryStream.cs @@ -88,6 +88,7 @@ namespace VNLib.Utils.IO /// The readonly stream public static VnMemoryStream CreateReadonly(VnMemoryStream stream) { + ArgumentNullException.ThrowIfNull(stream); //Set the readonly flag stream._isReadonly = true; //Return the stream @@ -528,10 +529,19 @@ namespace VNLib.Utils.IO /// public byte[] ToArray() { - //Alloc a new array of the size of the internal buffer, may be 64 bit large block - byte[] data = new byte[_length]; - - //Copy the internal buffer to the new array + byte[] data; + + if (_length < Int32.MaxValue) + { + //Alloc uninialized, since were going to overwite it anyway + data = GC.AllocateUninitializedArray((int)_length, false); + } + else + { + //Use new opperator if larger than 32bit + data = new byte[_length]; + } + MemoryUtil.CopyArray(_buffer, 0, data, 0, (nuint)_length); return data; diff --git a/lib/Utils/src/Memory/Caching/IReusable.cs b/lib/Utils/src/Memory/Caching/IReusable.cs index 4472ad3..cf73fa0 100644 --- a/lib/Utils/src/Memory/Caching/IReusable.cs +++ b/lib/Utils/src/Memory/Caching/IReusable.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Utils @@ -25,7 +25,7 @@ namespace VNLib.Utils.Memory.Caching { /// - /// Allows for use within a , this object is intended to be reused heavily + /// An interface that exposes event hooks for use within an , managment. /// public interface IReusable { diff --git a/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs b/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs index bcc5be9..f196597 100644 --- a/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs +++ b/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs @@ -74,7 +74,7 @@ namespace VNLib.Utils.Memory public static bool RequiresPinning(nuint byteSize, bool forceAcceleration) { /* - * Pinnin is required, if reflected memmove is not supported on the platform + * Pinning is required, if reflected memmove is not supported on the platform * AND the size of the data to copy is larger than 32 bit width. * * Otherwise if accleration is forced, pinning will always be required. @@ -94,9 +94,9 @@ namespace VNLib.Utils.Memory } /* - * Why does this function exist. For centralized memmove operations primarily. + * Why does this function exist? For centralized memmove operations primarily. * - * When the block is known to be small, all of the brances in memmove can be + * When the block is known to be small, all of the branches in memmove can be * alot of overhead including the possability of Avx2 being used for really * small blocks if they are aligned. If the block is known to be small, we * can just skip all of that and use the fastest method for small blocks, @@ -118,7 +118,6 @@ namespace VNLib.Utils.Memory Debug.Assert(!Unsafe.IsNullRef(in dstByte), "Null destination reference passed to MemmoveByRef"); _fallbackMemmove.Memmove(in srcByte, ref dstByte, byteCount); - return; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] diff --git a/lib/Utils/src/Memory/MemoryUtil.cs b/lib/Utils/src/Memory/MemoryUtil.cs index 774aca3..ba25104 100644 --- a/lib/Utils/src/Memory/MemoryUtil.cs +++ b/lib/Utils/src/Memory/MemoryUtil.cs @@ -32,7 +32,6 @@ using System.Runtime.InteropServices; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics.X86; -using VNLib.Utils.Resources; using VNLib.Utils.Memory.Diagnostics; namespace VNLib.Utils.Memory @@ -86,6 +85,11 @@ namespace VNLib.Utils.Memory /// that will use the array pool before falling back to the . /// heap. /// + /// + /// This value is chosen to be just under the size the CLR will promote an array to the + /// LOH, we can assume any heap impl will have long-term performance than the LOH for + /// large allocations. + /// public const int MAX_UNSAFE_POOL_SIZE = 80 * 1024; //Cache the system page size @@ -314,7 +318,11 @@ namespace VNLib.Utils.Memory /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InitializeBlock(T[] array) where T : struct => InitializeBlock(array, (uint)array.Length); + public static void InitializeBlock(T[] array) where T : struct + { + ArgumentNullException.ThrowIfNull(array); + InitializeBlock(array, (uint)array.Length); + } /// /// Initializes the array with zeros up to the specified count @@ -327,7 +335,7 @@ namespace VNLib.Utils.Memory [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InitializeBlock(T[] array, uint count) where T: struct { - ArgumentNullException.ThrowIfNull(array, nameof(array)); + ArgumentNullException.ThrowIfNull(array); //Check bounds CheckBounds(array, 0, count); @@ -348,7 +356,7 @@ namespace VNLib.Utils.Memory [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static void InitializeBlock(ref T block, int itemCount) where T : struct { - ThrowIfNullRef(ref block, nameof(block)); + ThrowIfNullRef(in block, nameof(block)); if (itemCount <= 0) { @@ -406,8 +414,7 @@ namespace VNLib.Utils.Memory public static void ZeroStruct(T* structPtr) where T : unmanaged { ArgumentNullException.ThrowIfNull(structPtr); - - ZeroStruct(ref *structPtr); + ZeroStruct(ref Unsafe.AsRef(structPtr)); } /// @@ -431,14 +438,6 @@ namespace VNLib.Utils.Memory #region Copy - /* - * Dirty little trick to access internal Buffer.Memmove method for - * large references. May not always be supported, so optional safe - * guards are in place. - */ - private delegate void BigMemmove(ref byte dest, ref readonly byte src, nuint len); - private static readonly BigMemmove? _clrMemmove = ManagedLibrary.TryGetStaticMethod(typeof(Buffer), "Memmove", System.Reflection.BindingFlags.NonPublic); - /// /// Copies structure data from a source byte reference that points to a sequence of /// of data to the target structure reference. @@ -626,8 +625,11 @@ namespace VNLib.Utils.Memory { ArgumentNullException.ThrowIfNull(source); ArgumentOutOfRangeException.ThrowIfLessThan(target.Length, sizeof(T), nameof(target)); - - CopyStruct(ref *source, target); + + CopyStruct( + ref Unsafe.AsRef(source), + target + ); } /// @@ -677,7 +679,7 @@ namespace VNLib.Utils.Memory Unsafe.CopyBlockUnaligned( ref Refs.AsByte(ref target, 0), - ref Refs.AsByteR(in source, 0), + in Refs.AsByteR(in source, 0), (uint)sizeof(T) ); } @@ -1354,7 +1356,7 @@ namespace VNLib.Utils.Memory /// The size of the span (the size of the block) /// A span over the block of memory pointed to by the handle of the specified size [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Span GetSpan(ref MemoryHandle handle, int size) => new(handle.Pointer, size); + public static Span GetSpan(ref readonly MemoryHandle handle, int size) => new(handle.Pointer, size); /// /// Gets a over the block of memory pointed to by the supplied handle. diff --git a/lib/Utils/src/Memory/ProcessHeap.cs b/lib/Utils/src/Memory/ProcessHeap.cs index 35a2a71..f13181b 100644 --- a/lib/Utils/src/Memory/ProcessHeap.cs +++ b/lib/Utils/src/Memory/ProcessHeap.cs @@ -54,10 +54,7 @@ namespace VNLib.Utils.Memory /// /// Initalizes a new global (cross platform) process heap /// - public ProcessHeap() - { - Trace.WriteLine($"Default heap instnace created {GetHashCode():x}"); - } + public ProcessHeap() => Trace.WriteLine($"Default heap instnace created {GetHashCode():x}"); /// /// @@ -78,13 +75,10 @@ namespace VNLib.Utils.Memory return true; } - + /// - protected override void Free() - { - Trace.WriteLine($"Default heap instnace disposed {GetHashCode():x}"); - } - + protected override void Free() => Trace.WriteLine($"Default heap instnace disposed {GetHashCode():x}"); + /// /// /// diff --git a/lib/Utils/src/Native/SafeLibraryHandle.cs b/lib/Utils/src/Native/SafeLibraryHandle.cs index 6b1af00..ed9dd16 100644 --- a/lib/Utils/src/Native/SafeLibraryHandle.cs +++ b/lib/Utils/src/Native/SafeLibraryHandle.cs @@ -150,11 +150,19 @@ namespace VNLib.Utils.Native private static string? GetLibraryFile(string dirPath, string libPath, SearchOption search) { - //slice the lib to its file name - libPath = Path.GetFileName(libPath); - libPath = Path.ChangeExtension(libPath, OperatingSystem.IsWindows() ? ".dll" : ".so"); - //Select the first file that matches the name - return Directory.EnumerateFiles(dirPath, libPath, search).FirstOrDefault(); + //If the library path already has an extension, just search for the file + if (Path.HasExtension(libPath)) + { + return Directory.EnumerateFiles(dirPath, libPath, search).FirstOrDefault(); + } + else + { + //slice the lib to its file name + libPath = Path.GetFileName(libPath); + libPath = Path.ChangeExtension(libPath, OperatingSystem.IsWindows() ? ".dll" : ".so"); + //Select the first file that matches the name + return Directory.EnumerateFiles(dirPath, libPath, search).FirstOrDefault(); + } } /// @@ -173,7 +181,7 @@ namespace VNLib.Utils.Native bool success = false; DangerousAddRef(ref success); - ObjectDisposedException.ThrowIf(success == false, "The libary has been released!"); + ObjectDisposedException.ThrowIf(success == false, this); try { diff --git a/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs b/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs index 01b985e..6e4f024 100644 --- a/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs +++ b/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs @@ -71,9 +71,7 @@ namespace McMaster.NETCore.Plugins.Loader _preferDefaultLoadContext = preferDefaultLoadContext; _loadInMemory = loadInMemory; - _resourceRoots = new[] { _basePath } - .Concat(resourceProbingPaths) - .ToArray(); + _resourceRoots = [_basePath, .. resourceProbingPaths]; _shadowCopyNativeLibraries = shadowCopyNativeLibraries; _unmanagedDllShadowCopyDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); -- cgit