aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-05-12 16:55:32 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2024-05-12 16:55:32 -0400
commit4035c838c1508af0aa7e767a97431a692958ce1c (patch)
tree9c8f719db15364296fb9b18cbe559a001d925d73 /lib
parentf4f0d4f74250257991c57bfae74c4852c7e1ae46 (diff)
perf: Utils + http perf mods
Diffstat (limited to 'lib')
-rw-r--r--lib/Net.Http/src/Core/ConnectionInfo.cs26
-rw-r--r--lib/Net.Http/src/Core/HttpCookie.cs121
-rw-r--r--lib/Net.Http/src/Core/HttpServerProcessing.cs18
-rw-r--r--lib/Net.Http/src/Core/IHttpContextInformation.cs3
-rw-r--r--lib/Net.Http/src/Core/PerfCounter/HttpPerfCounter.cs76
-rw-r--r--lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs32
-rw-r--r--lib/Net.Http/src/Core/Request/HttpRequest.cs7
-rw-r--r--lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs2
-rw-r--r--lib/Net.Http/src/Core/Response/HttpResponse.cs30
-rw-r--r--lib/Net.Http/src/Core/Response/ResponseWriter.cs27
-rw-r--r--lib/Net.Http/src/Helpers/HttpControlMask.cs4
-rw-r--r--lib/Net.Http/src/Helpers/HttpHelpers.cs14
-rw-r--r--lib/Net.Http/src/HttpConfig.cs7
-rw-r--r--lib/Net.Http/src/HttpResponseCookie.cs226
-rw-r--r--lib/Net.Http/src/IConnectionInfo.cs11
-rw-r--r--lib/Net.Messaging.FBM/src/Client/FBMRequest.cs4
-rw-r--r--lib/Net.Rest.Client/src/Construction/Extensions.cs98
-rw-r--r--lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs12
-rw-r--r--lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs131
-rw-r--r--lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs4
-rw-r--r--lib/Plugins.Essentials/src/Extensions/HttpCookie.cs3
-rw-r--r--lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs15
-rw-r--r--lib/Plugins.Essentials/src/HttpEntity.cs54
-rw-r--r--lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs23
-rw-r--r--lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs24
-rw-r--r--lib/Utils/src/Extensions/MemoryExtensions.cs45
-rw-r--r--lib/Utils/src/Extensions/StringExtensions.cs6
-rw-r--r--lib/Utils/src/Memory/ForwardOnlyWriter.cs108
-rw-r--r--lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs5
29 files changed, 805 insertions, 331 deletions
diff --git a/lib/Net.Http/src/Core/ConnectionInfo.cs b/lib/Net.Http/src/Core/ConnectionInfo.cs
index bcc5fe7..4a46971 100644
--- a/lib/Net.Http/src/Core/ConnectionInfo.cs
+++ b/lib/Net.Http/src/Core/ConnectionInfo.cs
@@ -88,33 +88,17 @@ namespace VNLib.Net.Http
public IReadOnlyCollection<string> Accept => Context.Request.Accept;
///<inheritdoc/>
- public ref readonly TransportSecurityInfo? GetTransportSecurityInfo() => ref Context.GetSecurityInfo();
+ public ref readonly TransportSecurityInfo? GetTransportSecurityInfo() => ref Context.GetSecurityInfo();
///<inheritdoc/>
- public void SetCookie(string name, string value, string? domain, string? path, TimeSpan Expires, CookieSameSite sameSite, bool httpOnly, bool secure)
+ public void SetCookie(in HttpResponseCookie cookie)
{
//name MUST not be null
- ArgumentNullException.ThrowIfNull(name);
-
- //Create the new cookie
- HttpCookie cookie = new(name)
- {
- Value = value,
- Domain = domain,
- Path = path,
- MaxAge = Expires,
- //Set the session lifetime flag if the timeout is max value
- IsSession = Expires == TimeSpan.MaxValue,
- //If the connection is cross origin, then we need to modify the secure and samsite values
- SameSite = CrossOrigin ? CookieSameSite.None : sameSite,
- Secure = secure | CrossOrigin,
- HttpOnly = httpOnly
- };
-
- //Set the cookie
+ ArgumentException.ThrowIfNullOrWhiteSpace(cookie.Name, nameof(cookie.Name));
+
Context.Response.AddCookie(in cookie);
}
-
+
internal ConnectionInfo(HttpContext ctx)
{
//Update the context referrence
diff --git a/lib/Net.Http/src/Core/HttpCookie.cs b/lib/Net.Http/src/Core/HttpCookie.cs
deleted file mode 100644
index b805e3e..0000000
--- a/lib/Net.Http/src/Core/HttpCookie.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
-* Copyright (c) 2024 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Net.Http
-* File: HttpCookie.cs
-*
-* HttpCookie.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/.
-*/
-
-using System;
-
-using VNLib.Utils;
-using VNLib.Utils.Memory;
-using VNLib.Utils.Extensions;
-
-namespace VNLib.Net.Http.Core
-{
- internal readonly struct HttpCookie(string name) : IStringSerializeable, IEquatable<HttpCookie>
- {
- public readonly string Name { get; } = name;
- public readonly string? Value { get; init; }
- public readonly string? Domain { get; init; }
- public readonly string? Path { get; init; }
- public readonly TimeSpan MaxAge { get; init; }
- public readonly CookieSameSite SameSite { get; init; }
- public readonly bool Secure { get; init; }
- public readonly bool HttpOnly { get; init; }
- public readonly bool IsSession { get; init; }
-
- public readonly string Compile() => throw new NotImplementedException();
-
- public readonly void Compile(ref ForwardOnlyWriter<char> writer)
- {
- //set the name of the cookie
- writer.Append(Name);
- writer.Append('=');
-
- //set name
- writer.Append(Value);
-
- //Only set the max age parameter if the cookie is not a session cookie
- if (!IsSession)
- {
- writer.Append("; Max-Age=");
- writer.Append((int)MaxAge.TotalSeconds);
- }
-
- //Make sure domain is set
- if (!string.IsNullOrWhiteSpace(Domain))
- {
- writer.Append("; Domain=");
- writer.Append(Domain);
- }
-
- //Check and set path
- if (!string.IsNullOrWhiteSpace(Path))
- {
- //Set path
- writer.Append("; Path=");
- writer.Append(Path);
- }
-
- writer.Append("; SameSite=");
-
- //Set the samesite flag based on the enum value
- switch (SameSite)
- {
- case CookieSameSite.None:
- writer.Append("None");
- break;
- case CookieSameSite.Strict:
- writer.Append("Strict");
- break;
- case CookieSameSite.Lax:
- default:
- writer.Append("Lax");
- break;
- }
-
- //Set httponly flag
- if (HttpOnly)
- {
- writer.Append("; HttpOnly");
- }
-
- //Set secure flag
- if (Secure)
- {
- writer.Append("; Secure");
- }
- }
-
- public readonly ERRNO Compile(Span<char> buffer)
- {
- ForwardOnlyWriter<char> writer = new(buffer);
- Compile(ref writer);
- return writer.Written;
- }
-
- public readonly override int GetHashCode() => string.GetHashCode(Name, StringComparison.OrdinalIgnoreCase);
-
- public readonly override bool Equals(object? obj) => obj is HttpCookie other && Equals(other);
-
- public readonly bool Equals(HttpCookie other) => Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase);
- }
-} \ No newline at end of file
diff --git a/lib/Net.Http/src/Core/HttpServerProcessing.cs b/lib/Net.Http/src/Core/HttpServerProcessing.cs
index 7770ad7..b6dbfef 100644
--- a/lib/Net.Http/src/Core/HttpServerProcessing.cs
+++ b/lib/Net.Http/src/Core/HttpServerProcessing.cs
@@ -34,9 +34,11 @@ using System.Runtime.CompilerServices;
using VNLib.Utils.Memory;
using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
using VNLib.Net.Http.Core;
using VNLib.Net.Http.Core.Buffering;
using VNLib.Net.Http.Core.Response;
+using VNLib.Net.Http.Core.PerfCounter;
namespace VNLib.Net.Http
{
@@ -167,14 +169,20 @@ namespace VNLib.Net.Http
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private async Task<bool> ProcessHttpEventAsync(HttpContext context)
{
+ HttpPerfCounterState counter = default;
+
//Prepare http context to process a new message
context.BeginRequest();
try
{
+ HttpPerfCounter.StartCounter(ref counter);
+
//Try to parse the http request (may throw exceptions, let them propagate to the transport layer)
int status = (int)ParseRequest(context);
+ HttpPerfCounter.StopAndLog(ref counter, in _config, "HTTP Parse");
+
//Check status code for socket error, if so, return false to close the connection
if (status >= 1000)
{
@@ -204,7 +212,7 @@ namespace VNLib.Net.Http
context.Request.Compile(ref writer);
//newline
- writer.Append("\r\n");
+ writer.AppendSmall("\r\n");
//Response
context.Response.Compile(ref writer);
@@ -218,11 +226,15 @@ namespace VNLib.Net.Http
WriteConnectionDebugLog(this, context);
}
#endif
+
+ HttpPerfCounter.StartCounter(ref counter);
await context.WriteResponseAsync();
-
+
await context.FlushTransportAsync();
-
+
+ HttpPerfCounter.StopAndLog(ref counter, in _config, "HTTP Response");
+
/*
* If an alternate protocol was specified, we need to break the keepalive loop
* the handler will manage the alternate protocol
diff --git a/lib/Net.Http/src/Core/IHttpContextInformation.cs b/lib/Net.Http/src/Core/IHttpContextInformation.cs
index 14067f5..38e86b3 100644
--- a/lib/Net.Http/src/Core/IHttpContextInformation.cs
+++ b/lib/Net.Http/src/Core/IHttpContextInformation.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Net.Http
@@ -22,7 +22,6 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
-using System;
using System.IO;
using System.Text;
diff --git a/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounter.cs b/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounter.cs
new file mode 100644
index 0000000..0dcefb1
--- /dev/null
+++ b/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounter.cs
@@ -0,0 +1,76 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Net.Http
+* File: HttpPerfCounter.cs
+*
+* HttpPerfCounter.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/.
+*/
+
+using System;
+using System.Diagnostics;
+
+using VNLib.Utils.Logging;
+
+
+namespace VNLib.Net.Http.Core.PerfCounter
+{
+ internal static class HttpPerfCounter
+ {
+
+ [Conditional("DEBUG")]
+ internal static void StartCounter(ref HttpPerfCounterState state)
+ {
+ state.StopValue = state.StartValue = TimeProvider.System.GetTimestamp();
+ }
+
+ [Conditional("DEBUG")]
+ internal static void StopCounter(ref HttpPerfCounterState state)
+ {
+ state.StopValue = TimeProvider.System.GetTimestamp();
+ }
+
+ /// <summary>
+ /// Gets the total time elapsed in microseconds
+ /// </summary>
+ /// <returns>The time in microseconds that has elapsed since the timer was started and stopped</returns>
+ internal static TimeSpan GetElapsedTime(ref readonly HttpPerfCounterState state)
+ => TimeProvider.System.GetElapsedTime(state.StartValue, state.StopValue);
+
+ /*
+ * Enable http performance counters for tracing.
+ * Only available in debug builds until it can be
+ * configured for zero-cost
+ */
+
+ [Conditional("DEBUG")]
+ internal static void StopAndLog(ref HttpPerfCounterState state, ref readonly HttpConfig config, string counter)
+ {
+ if (!config.DebugPerformanceCounters)
+ {
+ return;
+ }
+
+ StopCounter(ref state);
+
+ TimeSpan duration = GetElapsedTime(in state);
+
+ config.ServerLog.Debug("[PERF]: ({state}) - {us}us elapsed", counter, duration.TotalMicroseconds);
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs b/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs
new file mode 100644
index 0000000..a86ac40
--- /dev/null
+++ b/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs
@@ -0,0 +1,32 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Net.Http
+* File: ConnectionInfo.cs
+*
+* ConnectionInfo.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/.
+*/
+
+namespace VNLib.Net.Http.Core.PerfCounter
+{
+ internal struct HttpPerfCounterState
+ {
+ internal long StartValue;
+ internal long StopValue;
+ }
+} \ No newline at end of file
diff --git a/lib/Net.Http/src/Core/Request/HttpRequest.cs b/lib/Net.Http/src/Core/Request/HttpRequest.cs
index 3ebf0d4..2c9eed0 100644
--- a/lib/Net.Http/src/Core/Request/HttpRequest.cs
+++ b/lib/Net.Http/src/Core/Request/HttpRequest.cs
@@ -161,11 +161,12 @@ namespace VNLib.Net.Http.Core
{
return Array.Empty<FileUpload>();
}
+
//Create new array to hold uploads
- FileUpload[] uploads = new FileUpload[_state.UploadCount];
- //Copy uploads to new array
+ FileUpload[] uploads = GC.AllocateUninitializedArray<FileUpload>(_state.UploadCount, false);
+
Array.Copy(_uploads, uploads, _state.UploadCount);
- //Return new array
+
return uploads;
}
diff --git a/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs b/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs
index dcd0553..93ce5b2 100644
--- a/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs
+++ b/lib/Net.Http/src/Core/Response/HttpContextResponseWriting.cs
@@ -93,7 +93,7 @@ namespace VNLib.Net.Http.Core
//Determine if compression should be used
bool compressionDisabled =
//disabled because app code disabled it
- ContextFlags.IsSet(HttpControlMask.CompressionDisabed)
+ ContextFlags.IsSet(HttpControlMask.CompressionDisabled)
//Disabled because too large or too small
|| length >= ParentServer.Config.CompressionLimit
|| length < ParentServer.Config.CompressionMinimum
diff --git a/lib/Net.Http/src/Core/Response/HttpResponse.cs b/lib/Net.Http/src/Core/Response/HttpResponse.cs
index 1340dac..e354998 100644
--- a/lib/Net.Http/src/Core/Response/HttpResponse.cs
+++ b/lib/Net.Http/src/Core/Response/HttpResponse.cs
@@ -46,7 +46,7 @@ namespace VNLib.Net.Http.Core.Response
{
const int DefaultCookieCapacity = 2;
- private readonly Dictionary<string, HttpCookie> Cookies = new(DefaultCookieCapacity, StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary<string, HttpResponseCookie> Cookies = new(DefaultCookieCapacity, StringComparer.OrdinalIgnoreCase);
private readonly DirectStream ReusableDirectStream = new();
private readonly ChunkedStream ReusableChunkedStream = new(manager.ChunkAccumulatorBuffer, ContextInfo);
private readonly HeaderDataAccumulator Writer = new(manager.ResponseHeaderBuffer, ContextInfo);
@@ -61,7 +61,7 @@ namespace VNLib.Net.Http.Core.Response
/// <summary>
/// Response header collection
/// </summary>
- public VnWebHeaderCollection Headers { get; } = [];
+ public readonly VnWebHeaderCollection Headers = [];
/// <summary>
/// The current http status code value
@@ -88,7 +88,7 @@ namespace VNLib.Net.Http.Core.Response
/// </summary>
/// <param name="cookie">Cookie to add</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal void AddCookie(in HttpCookie cookie) => Cookies[cookie.Name] = cookie;
+ internal void AddCookie(ref readonly HttpResponseCookie cookie) => Cookies[cookie.Name] = cookie;
/// <summary>
/// Compiles and flushes all headers to the header accumulator ready for sending
@@ -106,13 +106,13 @@ namespace VNLib.Net.Http.Core.Response
if (!HeadersBegun)
{
//write status code first
- writer.Append(HttpHelpers.GetResponseString(ContextInfo.CurrentVersion, _code));
- writer.Append(HttpHelpers.CRLF);
+ writer.AppendSmall(HttpHelpers.GetResponseString(ContextInfo.CurrentVersion, _code));
+ writer.AppendSmall(HttpHelpers.CRLF);
//Write the date to header buffer
- writer.Append("Date: ");
+ writer.AppendSmall("Date: ");
writer.Append(DateTimeOffset.UtcNow, "R");
- writer.Append(HttpHelpers.CRLF);
+ writer.AppendSmall(HttpHelpers.CRLF);
//Set begun flag
HeadersBegun = true;
@@ -122,10 +122,10 @@ namespace VNLib.Net.Http.Core.Response
for (int i = 0; i < Headers.Count; i++)
{
//<name>: <value>\r\n
- writer.Append(Headers.Keys[i]);
- writer.Append(": ");
- writer.Append(Headers[i]);
- writer.Append(HttpHelpers.CRLF);
+ writer.Append(Headers.Keys[i]);
+ writer.AppendSmall(": ");
+ writer.Append(Headers[i]);
+ writer.AppendSmall(HttpHelpers.CRLF);
}
//Remove writen headers
@@ -134,14 +134,14 @@ namespace VNLib.Net.Http.Core.Response
//Write cookies if any are set
if (Cookies.Count > 0)
{
- foreach (HttpCookie cookie in Cookies.Values)
+ foreach (HttpResponseCookie cookie in Cookies.Values)
{
- writer.Append("Set-Cookie: ");
+ writer.AppendSmall("Set-Cookie: ");
//Write the cookie to the header buffer
cookie.Compile(ref writer);
- writer.Append(HttpHelpers.CRLF);
+ writer.AppendSmall(HttpHelpers.CRLF);
}
Cookies.Clear();
@@ -431,7 +431,7 @@ namespace VNLib.Net.Http.Core.Response
}
//Enumerate and write
- foreach (HttpCookie cookie in Cookies.Values)
+ foreach (HttpResponseCookie cookie in Cookies.Values)
{
writer.Append("Set-Cookie: ");
diff --git a/lib/Net.Http/src/Core/Response/ResponseWriter.cs b/lib/Net.Http/src/Core/Response/ResponseWriter.cs
index b60537d..fabdff3 100644
--- a/lib/Net.Http/src/Core/Response/ResponseWriter.cs
+++ b/lib/Net.Http/src/Core/Response/ResponseWriter.cs
@@ -182,30 +182,33 @@ namespace VNLib.Net.Http.Core.Response
{
if (blockSize > 0)
{
+ /*
+ * Write data directly from memory response but fix the block size to the size
+ * of the compressor if it has one, to optimize compression
+ */
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
- {
- //Write response body from memory
+ {
+ /*
+ * Compressor block size is unkown so we can assume it does not matter
+ * and write full blocks as they are read. This will usually be a on-shot
+ * operation, since the writer handles chunk buffering
+ */
+
while (_userState.MemResponse.Remaining > 0)
{
- //Get remaining segment
_readSegment = _userState.MemResponse.GetMemory();
-
- //Write segment to output stream
+
await dest.WriteAsync(_readSegment);
-
- //Advance by the written amount
+
_userState.MemResponse.Advance(_readSegment.Length);
}
}
diff --git a/lib/Net.Http/src/Helpers/HttpControlMask.cs b/lib/Net.Http/src/Helpers/HttpControlMask.cs
index a2a004d..e24d088 100644
--- a/lib/Net.Http/src/Helpers/HttpControlMask.cs
+++ b/lib/Net.Http/src/Helpers/HttpControlMask.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Net.Http
@@ -33,7 +33,7 @@ namespace VNLib.Net.Http
/// <summary>
/// Tells the http server that dynamic response compression should be disabled
/// </summary>
- public const ulong CompressionDisabed = 0x01UL;
+ public const ulong CompressionDisabled = 0x01UL;
/// <summary>
/// Tells the server not to set a 0 content length header when sending a response that does
diff --git a/lib/Net.Http/src/Helpers/HttpHelpers.cs b/lib/Net.Http/src/Helpers/HttpHelpers.cs
index 86616f8..cf8e189 100644
--- a/lib/Net.Http/src/Helpers/HttpHelpers.cs
+++ b/lib/Net.Http/src/Helpers/HttpHelpers.cs
@@ -227,29 +227,29 @@ namespace VNLib.Net.Http
ForwardOnlyWriter<char> sb = new(buffer);
if ((type & CacheType.NoCache) > 0)
{
- sb.Append("no-cache, ");
+ sb.AppendSmall("no-cache, ");
}
if ((type & CacheType.NoStore) > 0)
{
- sb.Append("no-store, ");
+ sb.AppendSmall("no-store, ");
}
if ((type & CacheType.Public) > 0)
{
- sb.Append("public, ");
+ sb.AppendSmall("public, ");
}
if ((type & CacheType.Private) > 0)
{
- sb.Append("private, ");
+ sb.AppendSmall("private, ");
}
if ((type & CacheType.Revalidate) > 0)
{
- sb.Append("must-revalidate, ");
+ sb.AppendSmall("must-revalidate, ");
}
if (immutable)
{
- sb.Append("immutable, ");
+ sb.AppendSmall("immutable, ");
}
- sb.Append("max-age=");
+ sb.AppendSmall("max-age=");
sb.Append(maxAge);
return sb.ToString();
}
diff --git a/lib/Net.Http/src/HttpConfig.cs b/lib/Net.Http/src/HttpConfig.cs
index 274e163..c74bdbb 100644
--- a/lib/Net.Http/src/HttpConfig.cs
+++ b/lib/Net.Http/src/HttpConfig.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Net.Http
@@ -124,5 +124,10 @@ namespace VNLib.Net.Http
/// the server.
/// </summary>
public readonly IHttpCompressorManager? CompressorManager { get; init; } = null;
+
+ /// <summary>
+ /// Enables debug performance counters
+ /// </summary>
+ public readonly bool DebugPerformanceCounters { get; init; } = false;
}
} \ No newline at end of file
diff --git a/lib/Net.Http/src/HttpResponseCookie.cs b/lib/Net.Http/src/HttpResponseCookie.cs
new file mode 100644
index 0000000..8fc54c2
--- /dev/null
+++ b/lib/Net.Http/src/HttpResponseCookie.cs
@@ -0,0 +1,226 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Net.Http
+* File: HttpResponseCookie.cs
+*
+* HttpResponseCookie.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/.
+*/
+
+using System;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Net.Http
+{
+ /// <summary>
+ /// Represents an HTTP cookie that is set with responses.
+ /// </summary>
+ /// <param name="name">The cookie name</param>
+ public readonly struct HttpResponseCookie(string name) : IStringSerializeable, IEquatable<HttpResponseCookie>
+ {
+ /// <summary>
+ /// The default copy buffer allocated when calling the <see cref="Compile()"/>
+ /// family of methods.
+ /// </summary>
+ public const int DefaultCookieBufferSize = 4096;
+
+
+ /// <summary>
+ /// The name of the cookie to set.
+ /// </summary>
+ public readonly string Name { get; } = name;
+
+ /// <summary>
+ /// The actual cookie content or value.
+ /// </summary>
+ public readonly string? Value { get; init; }
+
+ /// <summary>
+ /// The domain this cookie will be sent to.
+ /// </summary>
+ public readonly string? Domain { get; init; }
+
+ /// <summary>
+ /// The cookie path the client will send this cookie with. Null
+ /// or empty string for all paths.
+ /// </summary>
+ public readonly string? Path { get; init; }
+
+ /// <summary>
+ /// Sets the duration of the cookie lifetime (in seconds), aka MaxAge
+ /// </summary>
+ public readonly TimeSpan MaxAge { get; init; }
+
+ /// <summary>
+ /// Sets the cookie Samesite field.
+ /// </summary>
+ public readonly CookieSameSite SameSite { get; init; }
+
+ /// <summary>
+ /// Sets the cookie Secure flag. If true only sends the cookie with requests
+ /// if the connection is secure.
+ /// </summary>
+ public readonly bool Secure { get; init; }
+
+ /// <summary>
+ /// Sets cookie HttpOnly flag. If true denies JavaScript access to
+ /// </summary>
+ public readonly bool HttpOnly { get; init; }
+
+ /// <summary>
+ /// Sets the cookie expiration to the duration of the user's session (aka no expiration)
+ /// </summary>
+ public readonly bool IsSession { get; init; }
+
+ /// <summary>
+ /// Creates an HTTP 1.x spec cookie header value from the
+ /// cookie fields
+ /// <para>
+ /// The internal copy buffer defaults to <see cref="DefaultCookieBufferSize"/>
+ /// use <see cref="Compile(int)"/> if you need control over the buffer size
+ /// </para>
+ /// </summary>
+ /// <returns>The cookie header value as a string</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public readonly string Compile()
+ {
+ nint bufSize = MemoryUtil.NearestPage(DefaultCookieBufferSize);
+
+ return Compile(bufSize.ToInt32());
+ }
+
+ /// <summary>
+ /// Creates an HTTP 1.x spec cookie header value from the
+ /// cookie fields.
+ /// </summary>
+ /// <param name="bufferSize">The size of the internal accumulator buffer</param>
+ /// <returns>The cookie header value as a string</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public readonly string Compile(int bufferSize)
+ {
+ using UnsafeMemoryHandle<char> cookieBuffer = MemoryUtil.UnsafeAlloc<char>(bufferSize, false);
+
+ ERRNO count = Compile(cookieBuffer.Span);
+
+ return cookieBuffer.AsSpan(0, (int)count).ToString();
+ }
+
+ /// <summary>
+ /// Creates an HTTP 1.x spec cookie header value from the
+ /// cookie fields.
+ /// </summary>
+ /// <param name="buffer">The character buffer to write the cookie data tor</param>
+ /// <returns>The cookie header value as a string</returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public readonly ERRNO Compile(Span<char> buffer)
+ {
+ ForwardOnlyWriter<char> writer = new(buffer);
+ Compile(ref writer);
+ return writer.Written;
+ }
+
+ /// <summary>
+ /// Writes the HTTP 1.x header format for the cookie
+ /// </summary>
+ /// <param name="writer"></param>
+ public readonly void Compile(ref ForwardOnlyWriter<char> writer)
+ {
+ writer.Append(Name);
+ writer.Append('=');
+ writer.Append(Value);
+
+ /*
+ * If a session cookie is set, then do not include a max-age value
+ * browsers will default to session duration if not set
+ */
+ if (!IsSession)
+ {
+ writer.AppendSmall("; Max-Age=");
+ writer.Append((int)MaxAge.TotalSeconds);
+ }
+
+ if (!string.IsNullOrWhiteSpace(Domain))
+ {
+ writer.AppendSmall("; Domain=");
+ writer.Append(Domain);
+ }
+
+ if (!string.IsNullOrWhiteSpace(Path))
+ {
+ //Set path
+ writer.AppendSmall("; Path=");
+ writer.Append(Path);
+ }
+
+ writer.AppendSmall("; SameSite=");
+
+ switch (SameSite)
+ {
+ case CookieSameSite.None:
+ writer.AppendSmall("None");
+ break;
+ case CookieSameSite.Strict:
+ writer.AppendSmall("Strict");
+ break;
+ case CookieSameSite.Lax:
+ default:
+ writer.AppendSmall("Lax");
+ break;
+ }
+
+ if (HttpOnly)
+ {
+ writer.AppendSmall("; HttpOnly");
+ }
+
+ if (Secure)
+ {
+ writer.AppendSmall("; Secure");
+ }
+ }
+
+ ///<inheritdoc/>
+ public readonly override int GetHashCode() => string.GetHashCode(Name, StringComparison.OrdinalIgnoreCase);
+
+ ///<inheritdoc/>
+ public readonly override bool Equals(object? obj) => obj is HttpResponseCookie other && Equals(other);
+
+ ///<inheritdoc/>
+ public readonly bool Equals(HttpResponseCookie other) => string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// Creates an HTTP 1.x spec cookie header value from the
+ /// cookie fields
+ /// <para>
+ /// The internal copy buffer defaults to <see cref="DefaultCookieBufferSize"/>
+ /// use <see cref="Compile(int)"/> if you need control over the buffer size
+ /// </para>
+ /// </summary>
+ /// <returns>The cookie header value as a string</returns>
+ public override string ToString() => Compile();
+
+ ///<inheritdoc/>
+ public static bool operator ==(HttpResponseCookie left, HttpResponseCookie right) => left.Equals(right);
+
+ ///<inheritdoc/>
+ public static bool operator !=(HttpResponseCookie left, HttpResponseCookie right) => !(left == right);
+ }
+} \ No newline at end of file
diff --git a/lib/Net.Http/src/IConnectionInfo.cs b/lib/Net.Http/src/IConnectionInfo.cs
index 6cdb480..7598864 100644
--- a/lib/Net.Http/src/IConnectionInfo.cs
+++ b/lib/Net.Http/src/IConnectionInfo.cs
@@ -133,14 +133,7 @@ namespace VNLib.Net.Http
/// Adds a new cookie to the response. If a cookie with the same name and value
/// has been set, the old cookie is replaced with the new one.
/// </summary>
- /// <param name="name">Cookie name/id</param>
- /// <param name="value">Value to be stored in cookie</param>
- /// <param name="domain">Domain for cookie to operate</param>
- /// <param name="path">Path to store cookie</param>
- /// <param name="Expires">Timespan representing how long the cookie should exist</param>
- /// <param name="sameSite">Samesite attribute, Default = Lax</param>
- /// <param name="httpOnly">Specify the HttpOnly flag</param>
- /// <param name="secure">Specify the Secure flag</param>
- void SetCookie(string name, string value, string? domain, string? path, TimeSpan Expires, CookieSameSite sameSite, bool httpOnly, bool secure);
+ /// <param name="cookie">A reference to the cookie to set on the current response</param>
+ void SetCookie(in HttpResponseCookie cookie);
}
} \ No newline at end of file
diff --git a/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs b/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs
index f16a490..e545d55 100644
--- a/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs
+++ b/lib/Net.Messaging.FBM/src/Client/FBMRequest.cs
@@ -277,9 +277,9 @@ namespace VNLib.Net.Messaging.FBM.Client
public void Compile(ref ForwardOnlyWriter<char> writer)
{
ReadOnlyMemory<byte> requestData = GetRequestData();
- writer.Append("Message ID:");
+ writer.AppendSmall("Message ID:");
writer.Append(MessageId);
- writer.Append(Environment.NewLine);
+ writer.AppendSmall(Environment.NewLine);
Helpers.DefaultEncoding.GetChars(requestData.Span, ref writer);
}
///<inheritdoc/>
diff --git a/lib/Net.Rest.Client/src/Construction/Extensions.cs b/lib/Net.Rest.Client/src/Construction/Extensions.cs
index ca0873b..93a1365 100644
--- a/lib/Net.Rest.Client/src/Construction/Extensions.cs
+++ b/lib/Net.Rest.Client/src/Construction/Extensions.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Net.Rest.Client
@@ -23,6 +23,7 @@
*/
using System;
+using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -46,8 +47,11 @@ namespace VNLib.Net.Rest.Client.Construction
/// <param name="entity">The request entity model to send to the server</param>
/// <param name="cancellation">A token to cancel the operation</param>
/// <returns>A task that resolves the response message</returns>
+ /// <exception cref="ArgumentNullException"></exception>
public static async Task<RestResponse> ExecuteAsync<TModel>(this IRestSiteAdapter site, TModel entity, CancellationToken cancellation = default)
{
+ ArgumentNullException.ThrowIfNull(site);
+
//Get the adapter for the model
IRestEndpointAdapter<TModel> adapter = site.GetAdapter<TModel>();
@@ -76,6 +80,44 @@ namespace VNLib.Net.Rest.Client.Construction
}
/// <summary>
+ /// Begins a stream download of the desired resource by sending the request model parameter.
+ /// An <see cref="IRestEndpointAdapter{TModel}"/> must be defined to handle requests of the given model type.
+ /// <para>
+ /// WARNING: This function will not invoke the OnResponse handler functions after the stream
+ /// has been returned, there is no way to inspect the response when excuting a stream download
+ /// </para>
+ /// </summary>
+ /// <typeparam name="TModel"></typeparam>
+ /// <param name="site"></param>
+ /// <param name="entity">The request entity model to send to the server</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A task that resolves the response data stream</returns>
+ public static async Task<Stream?> DownloadStreamAsync<TModel>(this IRestSiteAdapter site, TModel entity, CancellationToken cancellation = default)
+ {
+ ArgumentNullException.ThrowIfNull(site);
+
+ //Get the adapter for the model
+ IRestEndpointAdapter<TModel> adapter = site.GetAdapter<TModel>();
+
+ //Get new request on adapter
+ RestRequest request = adapter.GetRequest(entity);
+
+ //Wait to exec operations if needed
+ await site.WaitAsync(cancellation);
+
+ Stream? response;
+
+ //Get rest client
+ using (ClientContract contract = site.GetClient())
+ {
+ //Exec response
+ response = await contract.Resource.DownloadStreamAsync(request, cancellation);
+ }
+
+ return response;
+ }
+
+ /// <summary>
/// Executes a request against the site by sending the request model parameter. An <see cref="IRestEndpointAdapter{TModel}"/> must be
/// defined to handle requests of the given model type.
/// </summary>
@@ -87,6 +129,8 @@ namespace VNLib.Net.Rest.Client.Construction
/// <returns>A task that resolves the response message with json resonse support</returns>
public static async Task<RestResponse<TJson>> ExecuteAsync<TModel, TJson>(this IRestSiteAdapter site, TModel entity, CancellationToken cancellation = default)
{
+ ArgumentNullException.ThrowIfNull(site);
+
//Get the adapter for the model
IRestEndpointAdapter<TModel> adapter = site.GetAdapter<TModel>();
@@ -124,6 +168,8 @@ namespace VNLib.Net.Rest.Client.Construction
/// <returns>When completed, gets the <see cref="RestResponse"/></returns>
public static async Task<RestResponse> ExecuteSingleAsync<TModel>(this IRestSiteAdapter site, TModel model, CancellationToken cancellation = default) where TModel : IRestSingleEndpoint
{
+ ArgumentNullException.ThrowIfNull(site);
+
//Init new request
RestRequest request = new(model.Url, model.Method);
model.OnRequest(request);
@@ -149,6 +195,7 @@ namespace VNLib.Net.Rest.Client.Construction
return response;
}
+
/// <summary>
/// Sets the request method of a new request
/// </summary>
@@ -358,22 +405,47 @@ namespace VNLib.Net.Rest.Client.Construction
/// <typeparam name="TResult">The json response entity type</typeparam>
/// <param name="response">The response task</param>
/// <returns>A task that resolves the deserialized entity type</returns>
- public static async Task<TResult?> AsJson<TResult>(this Task<RestResponse> response)
+ public static Task<TResult?> AsJson<TResult>(this Task<RestResponse> response)
+ => As(response, static r => JsonSerializer.Deserialize<TResult>(r.RawBytes));
+
+ /// <summary>
+ /// Converts a task that resolves a <see cref="RestResponse"/> to a task that deserializes
+ /// the response data as json.
+ /// </summary>
+ /// <param name="response">The response task</param>
+ /// <returns>A task that resolves the deserialized entity type</returns>
+ public static Task<byte[]?> AsBytes(this Task<RestResponse> response) => As(response, static p => p.RawBytes);
+
+ /// <summary>
+ /// Converts a task that resolves a <see cref="RestResponse"/> to a task that uses your
+ /// transformation function to create the result
+ /// </summary>
+ /// <param name="response">The response task</param>
+ /// <param name="callback">Your custom callback function used to transform the data</param>
+ /// <returns>A task that resolves the deserialized entity type</returns>
+ public static async Task<T> As<T>(this Task<RestResponse> response, Func<RestResponse, Task<T>> callback)
{
+ ArgumentNullException.ThrowIfNull(response);
+ ArgumentNullException.ThrowIfNull(callback);
+
RestResponse r = await response.ConfigureAwait(false);
- return JsonSerializer.Deserialize<TResult>(r.RawBytes);
+ return await callback(r).ConfigureAwait(false);
}
/// <summary>
- /// Converts a task that resolves a <see cref="RestResponse"/> to a task that deserializes
- /// the response data as json.
+ /// Converts a task that resolves a <see cref="RestResponse"/> to a task that uses your
+ /// transformation function to create the result
/// </summary>
/// <param name="response">The response task</param>
+ /// <param name="callback">Your custom callback function used to transform the data</param>
/// <returns>A task that resolves the deserialized entity type</returns>
- public static async Task<byte[]?> AsBytes(this Task<RestResponse> response)
+ public static async Task<T> As<T>(this Task<RestResponse> response, Func<RestResponse, T> callback)
{
+ ArgumentNullException.ThrowIfNull(response);
+ ArgumentNullException.ThrowIfNull(callback);
+
RestResponse r = await response.ConfigureAwait(false);
- return r.RawBytes;
+ return callback(r);
}
private record class EndpointAdapterBuilder(IRestSiteEndpointStore Site) : IRestEndpointBuilder
@@ -396,8 +468,8 @@ namespace VNLib.Net.Rest.Client.Construction
///<inheritdoc/>
public IRestRequestBuilder<TModel> WithModifier(Action<TModel, RestRequest> requestBuilder)
{
- _ = requestBuilder ?? throw new ArgumentNullException(nameof(requestBuilder));
- //Add handler to handler chain
+ ArgumentNullException.ThrowIfNull(requestBuilder);
+
Adapter.RequestChain.AddLast(requestBuilder);
return this;
}
@@ -405,8 +477,8 @@ namespace VNLib.Net.Rest.Client.Construction
///<inheritdoc/>
public IRestRequestBuilder<TModel> WithUrl(Func<TModel, string> uriBuilder)
{
- _ = uriBuilder ?? throw new ArgumentNullException(nameof(uriBuilder));
- //Add get url handler
+ ArgumentNullException.ThrowIfNull(uriBuilder);
+
Adapter.GetUrl = uriBuilder;
return this;
}
@@ -414,8 +486,8 @@ namespace VNLib.Net.Rest.Client.Construction
///<inheritdoc/>
public IRestRequestBuilder<TModel> OnResponse(Action<TModel, RestResponse> onResponseBuilder)
{
- _ = onResponseBuilder ?? throw new ArgumentNullException(nameof(onResponseBuilder));
- //Add a response handler
+ ArgumentNullException.ThrowIfNull(onResponseBuilder);
+
Adapter.ResponseChain.AddLast(onResponseBuilder);
return this;
}
diff --git a/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs b/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs
index b6df58c..cb3486f 100644
--- a/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs
+++ b/lib/Net.Transport.SimpleTCP/src/SocketPipeLineWorker.cs
@@ -562,16 +562,10 @@ namespace VNLib.Net.Transport.Tcp
void Stop();
}
- private readonly struct TpTimerWrapper : INetTimer
+ private readonly struct TpTimerWrapper(Timer timer, int timeout) : INetTimer
{
- private readonly Timer _timer;
- private readonly int _timeout;
-
- public TpTimerWrapper(Timer timer, int timeout)
- {
- _timer = timer;
- _timeout = timeout;
- }
+ private readonly Timer _timer = timer;
+ private readonly int _timeout = timeout;
public readonly void Start() => _timer.Restart(_timeout);
diff --git a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
index a99b1ab..64a9611 100644
--- a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs
@@ -114,6 +114,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <returns>true if the connection accepts any content typ, false otherwise</returns>
private static bool AcceptsAny(IConnectionInfo server)
{
+ // If no accept header is sent by clients, it is assumed it accepts all content types
if(server.Accept.Count == 0)
{
return true;
@@ -196,14 +197,15 @@ namespace VNLib.Plugins.Essentials.Extensions
//Alloc enough space to hold the string
Span<char> buffer = stackalloc char[64];
ForwardOnlyWriter<char> rangeBuilder = new(buffer);
+
//Build the range header in this format "bytes <begin>-<end>/<total>"
- rangeBuilder.Append("bytes ");
+ rangeBuilder.AppendSmall("bytes ");
rangeBuilder.Append(start);
rangeBuilder.Append('-');
rangeBuilder.Append(end);
rangeBuilder.Append('/');
rangeBuilder.Append(length);
- //Print to a string and set the content range header
+
entity.Server.Headers[HttpResponseHeader.ContentRange] = rangeBuilder.ToString();
}
@@ -212,7 +214,8 @@ namespace VNLib.Plugins.Essentials.Extensions
/// </summary>
/// <returns>true if the user-agent specified the cors security header</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsCors(this IConnectionInfo server) => "cors".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
+ public static bool IsCors(this IConnectionInfo server)
+ => string.Equals("cors", server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Determines if the User-Agent specified "cross-site" in the Sec-Site header, OR
@@ -223,8 +226,8 @@ namespace VNLib.Plugins.Essentials.Extensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsCrossSite(this IConnectionInfo server)
{
- return "cross-site".Equals(server.Headers[SEC_HEADER_SITE], StringComparison.OrdinalIgnoreCase)
- || (server.Origin != null && !server.RequestUri.DnsSafeHost.Equals(server.Origin.DnsSafeHost, StringComparison.Ordinal));
+ return string.Equals("cross-site", server.Headers[SEC_HEADER_SITE], StringComparison.OrdinalIgnoreCase)
+ || (server.Origin != null && ! string.Equals(server.RequestUri.DnsSafeHost, server.Origin.DnsSafeHost, StringComparison.Ordinal));
}
/// <summary>
@@ -233,14 +236,16 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="server"></param>
/// <returns>true if sec-user header was set to "?1"</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsUserInvoked(this IConnectionInfo server) => "?1".Equals(server.Headers[SEC_HEADER_USER], StringComparison.OrdinalIgnoreCase);
+ public static bool IsUserInvoked(this IConnectionInfo server)
+ => string.Equals("?1", server.Headers[SEC_HEADER_USER], StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Was this request created from normal user navigation
/// </summary>
/// <returns>true if sec-mode set to "navigate"</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsNavigation(this IConnectionInfo server) => "navigate".Equals(server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
+ public static bool IsNavigation(this IConnectionInfo server)
+ => string.Equals("navigate", server.Headers[SEC_HEADER_MODE], StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Determines if the client specified "no-cache" for the cache control header, signalling they do not wish to cache the entity
@@ -302,7 +307,11 @@ namespace VNLib.Plugins.Essentials.Extensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool RefererMatch(this IConnectionInfo server)
{
- return server.RequestUri.DnsSafeHost.Equals(server.Referer?.DnsSafeHost, StringComparison.OrdinalIgnoreCase);
+ return string.Equals(
+ server.RequestUri.DnsSafeHost,
+ server.Referer?.DnsSafeHost,
+ StringComparison.OrdinalIgnoreCase
+ );
}
/// <summary>
@@ -315,9 +324,25 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="sameSite"></param>
/// <param name="secure"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void ExpireCookie(this IConnectionInfo server, string name, string domain = "", string path = "/", CookieSameSite sameSite = CookieSameSite.None, bool secure = false)
+ public static void ExpireCookie(
+ this IConnectionInfo server,
+ string name,
+ string domain = "",
+ string path = "/",
+ CookieSameSite sameSite = CookieSameSite.None,
+ bool secure = false
+ )
{
- server.SetCookie(name, string.Empty, domain, path, TimeSpan.Zero, sameSite, false, secure);
+ SetCookie(
+ server: server,
+ name: name,
+ value: string.Empty,
+ domain: domain,
+ path: path,
+ expires: TimeSpan.Zero,
+ sameSite: sameSite,
+ secure: secure
+ );
}
/// <summary>
@@ -340,9 +365,20 @@ namespace VNLib.Plugins.Essentials.Extensions
string path = "/",
CookieSameSite sameSite = CookieSameSite.None,
bool httpOnly = false,
- bool secure = false)
+ bool secure = false
+ )
{
- server.SetCookie(name, value, domain, path, TimeSpan.MaxValue, sameSite, httpOnly, secure);
+ SetCookie(
+ server: server,
+ name: name,
+ value: value,
+ domain: domain,
+ path: path,
+ expires: TimeSpan.Zero,
+ sameSite: sameSite,
+ httpOnly: httpOnly,
+ secure: secure
+ );
}
/// <summary>
@@ -367,9 +403,24 @@ namespace VNLib.Plugins.Essentials.Extensions
string path = "/",
CookieSameSite sameSite = CookieSameSite.None,
bool httpOnly = false,
- bool secure = false)
+ bool secure = false
+ )
{
- server.SetCookie(name, value, domain, path, expires, sameSite, httpOnly, secure);
+
+ HttpResponseCookie cookie = new(name)
+ {
+ Value = value,
+ Domain = domain,
+ Path = path,
+ MaxAge = expires,
+ IsSession = expires == TimeSpan.MaxValue,
+ //If the connection is cross origin, then we need to modify the secure and samsite values
+ SameSite = sameSite,
+ HttpOnly = httpOnly,
+ Secure = secure | server.CrossOrigin,
+ };
+
+ server.SetCookie(in cookie);
}
@@ -380,35 +431,24 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="cookie">The cookie to set for the server</param>
/// <exception cref="ArgumentException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [Obsolete("HttpCookie type is obsolete in favor of HttpResponseCookie")]
public static void SetCookie(this IConnectionInfo server, in HttpCookie cookie)
{
- //Cookie name is required
- if(string.IsNullOrWhiteSpace(cookie.Name))
- {
- throw new ArgumentException("A nonn-null cookie name is required");
- }
-
//Set the cookie
- server.SetCookie(cookie.Name,
- cookie.Value,
- cookie.Domain,
- cookie.Path,
- cookie.ValidFor,
- cookie.SameSite,
- cookie.HttpOnly,
- cookie.Secure);
- }
-
- /// <summary>
- /// Is the current connection a "browser" ?
- /// </summary>
- /// <param name="server"></param>
- /// <returns>true if the user agent string contains "Mozilla" and does not contain "bot", false otherwise</returns>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsBrowser(this IConnectionInfo server)
- {
- //Get user-agent and determine if its a browser
- return server.UserAgent != null && !server.UserAgent.Contains("bot", StringComparison.OrdinalIgnoreCase) && server.UserAgent.Contains("Mozilla", StringComparison.OrdinalIgnoreCase);
+ HttpResponseCookie rCookie = new(cookie.Name)
+ {
+ Value = cookie.Value,
+ Domain = cookie.Domain,
+ Path = cookie.Path,
+ MaxAge = cookie.ValidFor,
+ IsSession = cookie.ValidFor == TimeSpan.MaxValue,
+ //If the connection is cross origin, then we need to modify the secure and samsite values
+ SameSite = cookie.SameSite,
+ HttpOnly = cookie.HttpOnly,
+ Secure = cookie.Secure | server.CrossOrigin,
+ };
+
+ server.SetCookie(in rCookie);
}
/// <summary>
@@ -417,12 +457,9 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="server"></param>
/// <returns>True of the connection was made from the local machine</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsLoopBack(this IConnectionInfo server)
- {
- IPAddress realIp = server.GetTrustedIp();
- return IPAddress.Any.Equals(realIp) || IPAddress.Loopback.Equals(realIp);
- }
-
+ public static bool IsLoopBack(this IConnectionInfo server)
+ => IPAddress.Loopback.Equals(GetTrustedIp(server));
+
/// <summary>
/// Did the connection set the dnt header?
/// </summary>
@@ -493,7 +530,7 @@ namespace VNLib.Plugins.Essentials.Extensions
//Standard https protocol header
string? protocol = server.Headers[X_FORWARDED_PROTO_HEADER];
//If the header is set and equals https then tls is being used
- return string.IsNullOrWhiteSpace(protocol) ? isSecure : "https".Equals(protocol, StringComparison.OrdinalIgnoreCase);
+ return string.IsNullOrWhiteSpace(protocol) ? isSecure : string.Equals("https", protocol, StringComparison.OrdinalIgnoreCase);
}
else
{
diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
index 8adf883..66155dd 100644
--- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
@@ -816,12 +816,12 @@ namespace VNLib.Plugins.Essentials.Extensions
public static string ContentTypeString(this in FileUpload upload) => HttpHelpers.GetContentTypeString(upload.ContentType);
/// <summary>
- /// Sets the <see cref="HttpControlMask.CompressionDisabed"/> flag on the current
+ /// Sets the <see cref="HttpControlMask.CompressionDisabled"/> flag on the current
/// <see cref="IHttpEvent"/> instance to disable dynamic compression on the response.
/// </summary>
/// <param name="entity"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void DisableCompression(this IHttpEvent entity) => entity.SetControlFlag(HttpControlMask.CompressionDisabed);
+ public static void DisableCompression(this IHttpEvent entity) => entity.SetControlFlag(HttpControlMask.CompressionDisabled);
/// <summary>
/// Attempts to upgrade the connection to a websocket, if the setup fails, it sets up the response to the client accordingly.
diff --git a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs
index 6158a69..19c8e78 100644
--- a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs
+++ b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -33,6 +33,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// </summary>
/// <param name="Name">The cookie name</param>
/// <param name="Value">The cookie value</param>
+ [Obsolete("Obsolete in favor of HttpResponseCookie")]
public readonly record struct HttpCookie (string Name, string Value)
{
/// <summary>
diff --git a/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs b/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs
index f3b02dc..c4b7619 100644
--- a/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs
+++ b/lib/Plugins.Essentials/src/Extensions/SingleCookieController.cs
@@ -94,15 +94,16 @@ namespace VNLib.Plugins.Essentials.Extensions
//Only set cooke if already exists or force is true
if (entity.Server.RequestCookies.ContainsKey(Name) || force)
{
- //Build and set cookie
- HttpCookie cookie = new(Name, value)
+ HttpResponseCookie cookie = new(Name)
{
- Secure = Secure,
- HttpOnly = HttpOnly,
- ValidFor = ValidFor,
- SameSite = SameSite,
+ Value = value,
+ Domain = Domain,
Path = Path,
- Domain = Domain
+ MaxAge = ValidFor,
+ IsSession = ValidFor == TimeSpan.MaxValue,
+ SameSite = SameSite,
+ HttpOnly = HttpOnly,
+ Secure = Secure | entity.Server.CrossOrigin,
};
entity.Server.SetCookie(in cookie);
diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs
index 2a24982..ff728e3 100644
--- a/lib/Plugins.Essentials/src/HttpEntity.cs
+++ b/lib/Plugins.Essentials/src/HttpEntity.cs
@@ -26,6 +26,7 @@ using System;
using System.IO;
using System.Net;
using System.Threading;
+using System.Diagnostics;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@@ -204,6 +205,27 @@ namespace VNLib.Plugins.Essentials
throw new ContentTypeUnacceptableException("The client does not accept the content type of the response");
}
+ /*
+ * If the underlying stream is actaully a memory stream,
+ * create a wrapper for it to read as a memory response.
+ * This is done to avoid a user-space copy since we can
+ * get access to access the internal buffer
+ *
+ * Stream length also should not cause an integer overflow,
+ * which also mean position is assumed not to overflow
+ * or cause an overflow during reading
+ */
+ if(stream is MemoryStream ms && length < int.MaxValue)
+ {
+ Entity.CloseResponse(
+ code,
+ type,
+ new MemStreamWrapper(ms, (int)length)
+ );
+
+ return;
+ }
+
Entity.CloseResponse(code, type, stream, length);
}
@@ -246,5 +268,37 @@ namespace VNLib.Plugins.Essentials
///<inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler) => Entity.DangerousChangeProtocol(protocolHandler);
+
+
+ private sealed class MemStreamWrapper(MemoryStream memStream, int length) : IMemoryResponseReader
+ {
+ readonly int length = length;
+
+ /*
+ * Stream may be offset by the caller, it needs
+ * to be respected during streaming.
+ */
+ int read = (int)memStream.Position;
+
+ public int Remaining
+ {
+ get
+ {
+ Debug.Assert(length - read >= 0);
+ return length - read;
+ }
+ }
+
+ public void Advance(int written) => read += written;
+
+ ///<inheritdoc/>
+ public void Close() => memStream.Dispose();
+
+ public ReadOnlyMemory<byte> GetMemory()
+ {
+ byte[] intBuffer = memStream.GetBuffer();
+ return new ReadOnlyMemory<byte>(intBuffer, read, Remaining);
+ }
+ }
}
}
diff --git a/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs b/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs
index 2ce62b3..c3a85c9 100644
--- a/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs
+++ b/lib/Plugins.Essentials/src/Middleware/MiddlewareController.cs
@@ -33,12 +33,20 @@ namespace VNLib.Plugins.Essentials.Middleware
public async ValueTask<bool> ProcessAsync(HttpEntity entity)
{
+ /*
+ * Loops through the current linkedlist of the current middleware chain. The
+ * chain should remain unmodified after GetCurrentHead() is called.
+ *
+ * Middleware will return a Continue routine to move to the next middleware
+ * node. All other routines mean the processor has responded to the client
+ * itself and must exit control and move to response.
+ */
+
LinkedListNode<IHttpMiddleware>? mwNode = _chain.GetCurrentHead();
//Loop through nodes
while (mwNode != null)
{
- //Invoke mw handler on our event
entity.EventArgs = await mwNode.ValueRef.ProcessAsync(entity);
switch (entity.EventArgs.Routine)
@@ -60,9 +68,14 @@ namespace VNLib.Plugins.Essentials.Middleware
public void PostProcess(HttpEntity entity)
{
- LinkedListNode<IHttpMiddleware>? mwNode = _chain.GetCurrentHead();
+ /*
+ * Middleware nodes may be allowed to inspect, or modify the return
+ * event arguments as the server may not have responded to the client
+ * yet.
+ */
- //Loop through nodes
+ LinkedListNode<IHttpMiddleware>? mwNode = _chain.GetCurrentHead();
+
while (mwNode != null)
{
//Invoke mw handler on our event
@@ -72,6 +85,4 @@ namespace VNLib.Plugins.Essentials.Middleware
}
}
}
-
-
-} \ No newline at end of file
+}
diff --git a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
index e65c26d..b60c7c3 100644
--- a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
+++ b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs
@@ -90,40 +90,40 @@ namespace VNLib.Plugins.Essentials.Oauth
ForwardOnlyWriter<char> writer = new(buffer.Span);
//Build the error message string
- writer.Append("{\"error\":\"");
+ writer.AppendSmall("{\"error\":\"");
switch (error)
{
case ErrorType.InvalidRequest:
- writer.Append("invalid_request");
+ writer.AppendSmall("invalid_request");
break;
case ErrorType.InvalidClient:
- writer.Append("invalid_client");
+ writer.AppendSmall("invalid_client");
break;
case ErrorType.UnauthorizedClient:
- writer.Append("unauthorized_client");
+ writer.AppendSmall("unauthorized_client");
break;
case ErrorType.InvalidToken:
- writer.Append("invalid_token");
+ writer.AppendSmall("invalid_token");
break;
case ErrorType.UnsupportedResponseType:
- writer.Append("unsupported_response_type");
+ writer.AppendSmall("unsupported_response_type");
break;
case ErrorType.InvalidScope:
- writer.Append("invalid_scope");
+ writer.AppendSmall("invalid_scope");
break;
case ErrorType.ServerError:
- writer.Append("server_error");
+ writer.AppendSmall("server_error");
break;
case ErrorType.TemporarilyUnavailable:
- writer.Append("temporarily_unavailable");
+ writer.AppendSmall("temporarily_unavailable");
break;
default:
- writer.Append("error");
+ writer.AppendSmall("error");
break;
}
- writer.Append("\",\"error_description\":\"");
+ writer.AppendSmall("\",\"error_description\":\"");
writer.Append(description);
- writer.Append("\"}");
+ writer.AppendSmall("\"}");
//Close the response with the json data
ev.CloseResponse(code, ContentType.Json, writer.AsSpan());
diff --git a/lib/Utils/src/Extensions/MemoryExtensions.cs b/lib/Utils/src/Extensions/MemoryExtensions.cs
index 65d90a0..083c7cf 100644
--- a/lib/Utils/src/Extensions/MemoryExtensions.cs
+++ b/lib/Utils/src/Extensions/MemoryExtensions.cs
@@ -578,6 +578,27 @@ namespace VNLib.Utils.Extensions
#region VnBufferWriter
/// <summary>
+ /// Appends the string value by copying it to the internal buffer
+ /// </summary>
+ /// <param name="buffer"></param>
+ /// <param name="value">The string value to append to the buffer</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Append(this ref ForwardOnlyWriter<char> buffer, string? value)
+ => buffer.Append(value.AsSpan());
+
+ /// <summary>
+ /// Appends the string value by copying it to the internal buffer
+ /// when the string is known to be very short.
+ /// </summary>
+ /// <param name="buffer"></param>
+ /// <param name="value">The string value to append to the buffer</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void AppendSmall(this ref ForwardOnlyWriter<char> buffer, string? value)
+ => buffer.AppendSmall(value.AsSpan());
+
+ /// <summary>
/// Formats and appends a value type to the writer with proper endianess
/// </summary>
/// <param name="buffer"></param>
@@ -636,12 +657,20 @@ namespace VNLib.Utils.Extensions
/// <param name="formatProvider"></param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Append<T>(this ref ForwardOnlyWriter<char> buffer, T value, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = default) where T : ISpanFormattable
+ public static void Append<T>(
+ this ref ForwardOnlyWriter<char> buffer,
+ T value,
+ ReadOnlySpan<char> format = default,
+ IFormatProvider? formatProvider = default
+ ) where T : ISpanFormattable
{
//Format value and write to buffer
if (!value.TryFormat(buffer.Remaining, out int charsWritten, format, formatProvider))
{
- throw new ArgumentOutOfRangeException(nameof(buffer), "The value could not be formatted and appended to the buffer, because there is not enough available space");
+ throw new ArgumentOutOfRangeException(
+ nameof(buffer),
+ "The value could not be formatted and appended to the buffer, because there is not enough available space"
+ );
}
//Update written posiion
buffer.Advance(charsWritten);
@@ -657,12 +686,20 @@ namespace VNLib.Utils.Extensions
/// <exception cref="OutOfMemoryException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Append<T>(this ref ForwardOnlyMemoryWriter<char> buffer, T value, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = default) where T : ISpanFormattable
+ public static void Append<T>(
+ this ref ForwardOnlyMemoryWriter<char> buffer,
+ T value,
+ ReadOnlySpan<char> format = default,
+ IFormatProvider? formatProvider = default
+ ) where T : ISpanFormattable
{
//Format value and write to buffer
if (!value.TryFormat(buffer.Remaining.Span, out int charsWritten, format, formatProvider))
{
- throw new ArgumentOutOfRangeException(nameof(buffer), "The value could not be formatted and appended to the buffer, because there is not enough available space");
+ throw new ArgumentOutOfRangeException(
+ nameof(buffer),
+ "The value could not be formatted and appended to the buffer, because there is not enough available space"
+ );
}
//Update written posiion
buffer.Advance(charsWritten);
diff --git a/lib/Utils/src/Extensions/StringExtensions.cs b/lib/Utils/src/Extensions/StringExtensions.cs
index c71d5a0..e9bbfbd 100644
--- a/lib/Utils/src/Extensions/StringExtensions.cs
+++ b/lib/Utils/src/Extensions/StringExtensions.cs
@@ -460,7 +460,7 @@ namespace VNLib.Utils.Extensions
public static int Replace(this Span<char> buffer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace)
{
ForwardOnlyWriter<char> writer = new (buffer);
- writer.Replace(search, replace);
+ Replace(ref writer, search, replace);
return writer.Written;
}
@@ -496,9 +496,9 @@ namespace VNLib.Utils.Extensions
do
{
//Append the data before the search chars
- writer2.Append(buffer[..start]);
+ writer2.Append<char>(buffer[..start]);
//Append the replacment
- writer2.Append(replace);
+ writer2.Append<char>(replace);
//Shift buffer to the end of the
buffer = buffer[(start + searchLen)..];
//search for next index beyond current index
diff --git a/lib/Utils/src/Memory/ForwardOnlyWriter.cs b/lib/Utils/src/Memory/ForwardOnlyWriter.cs
index d3c33a2..55e3b11 100644
--- a/lib/Utils/src/Memory/ForwardOnlyWriter.cs
+++ b/lib/Utils/src/Memory/ForwardOnlyWriter.cs
@@ -23,6 +23,7 @@
*/
using System;
+using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
namespace VNLib.Utils.Memory
@@ -30,12 +31,22 @@ namespace VNLib.Utils.Memory
/// <summary>
/// Provides a stack based buffer writer
/// </summary>
- public ref struct ForwardOnlyWriter<T>
+ /// <remarks>
+ /// Creates a new <see cref="ForwardOnlyWriter{T}"/> assigning the specified buffer
+ /// at the specified offset
+ /// </remarks>
+ /// <param name="buffer">The buffer to write data to</param>
+ /// <param name="offset">The offset to begin the writer at</param>
+ [method: MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ref struct ForwardOnlyWriter<T>(Span<T> buffer, int offset)
{
+ //Cache reference to the first value
+ private readonly ref T _basePtr = ref MemoryMarshal.GetReference(buffer);
+
/// <summary>
/// The buffer for writing output data to
/// </summary>
- public readonly Span<T> Buffer { get; }
+ public readonly Span<T> Buffer { get; } = buffer[offset..];
/// <summary>
/// The number of characters written to the buffer
@@ -57,43 +68,79 @@ namespace VNLib.Utils.Memory
/// </summary>
/// <param name="buffer">The buffer to write data to</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public ForwardOnlyWriter(Span<T> buffer)
- {
- Buffer = buffer;
- Written = 0;
- }
+ public ForwardOnlyWriter(Span<T> buffer): this(buffer, 0)
+ { }
/// <summary>
- /// Creates a new <see cref="ForwardOnlyWriter{T}"/> assigning the specified buffer
- /// at the specified offset
+ /// Returns a compiled string from the characters written to the buffer
/// </summary>
- /// <param name="buffer">The buffer to write data to</param>
- /// <param name="offset">The offset to begin the writer at</param>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public ForwardOnlyWriter(Span<T> buffer, int offset)
+ /// <returns>A string of the characters written to the buffer</returns>
+ public readonly override string ToString() => Buffer[..Written].ToString();
+
+ /// <summary>
+ /// Appends a sequence to the buffer
+ /// </summary>
+ /// <param name="data">The data sequence to append to the buffer</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public void Append<TClass>(ReadOnlySpan<T> data) where TClass : class, T
{
- Buffer = buffer[offset..];
- Written = 0;
+ //Make sure the current window is large enough to buffer the new string
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(data.Length, RemainingSize, nameof(Remaining));
+
+ //write data to window
+ data.CopyTo(Remaining);
+
+ //update char position
+ Written += data.Length;
}
/// <summary>
- /// Returns a compiled string from the characters written to the buffer
+ /// Appends a sequence to the buffer of a value type by copying source
+ /// memory to internal buffer memory
/// </summary>
- /// <returns>A string of the characters written to the buffer</returns>
- public readonly override string ToString() => Buffer[..Written].ToString();
+ /// <typeparam name="TStruct"></typeparam>
+ /// <param name="data">The data sequence to append to the buffer</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public void Append<TStruct>(ReadOnlySpan<TStruct> data) where TStruct : struct, T
+ {
+ //Make sure the current window is large enough to buffer the new string
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(data.Length, RemainingSize, nameof(Remaining));
+
+ //write data to window
+ MemoryUtil.Memmove(
+ in MemoryMarshal.GetReference(data),
+ 0,
+ ref Unsafe.As<T, TStruct>(ref _basePtr), //Reinterpret the ref to the local scope type,
+ (nuint)Written,
+ (nuint)data.Length
+ );
+
+ //update char position
+ Written += data.Length;
+ }
/// <summary>
- /// Appends a sequence to the buffer
+ /// Appends a sequence to the buffer of a value type by copying source
+ /// memory to internal buffer memory, when the buffer size is known to be
+ /// smaller than <see cref="ushort.MaxValue"/>.
/// </summary>
- /// <param name="data">The data to append to the buffer</param>
+ /// <typeparam name="TStruct"></typeparam>
+ /// <param name="data">The data sequence to append to the buffer</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
- public void Append(ReadOnlySpan<T> data)
+ public void AppendSmall<TStruct>(ReadOnlySpan<TStruct> data) where TStruct : struct, T
{
//Make sure the current window is large enough to buffer the new string
ArgumentOutOfRangeException.ThrowIfGreaterThan(data.Length, RemainingSize, nameof(Remaining));
- Span<T> window = Buffer[Written..];
+
//write data to window
- data.CopyTo(window);
+ MemoryUtil.SmallMemmove(
+ in MemoryMarshal.GetReference(data),
+ 0,
+ ref Unsafe.As<T, TStruct>(ref _basePtr), //Reinterpret the ref to the local scope type,
+ (nuint)Written,
+ (ushort)data.Length
+ );
+
//update char position
Written += data.Length;
}
@@ -103,12 +150,21 @@ namespace VNLib.Utils.Memory
/// </summary>
/// <param name="c">The item to append to the buffer</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(T c)
{
//Make sure the current window is large enough to buffer the new string
ArgumentOutOfRangeException.ThrowIfZero(RemainingSize);
- //Write data to buffer and increment the buffer position
- Buffer[Written++] = c;
+
+ /*
+ * Calc pointer to last written position.
+ * Written points to the address directly after the last written element
+ */
+
+ ref T offset = ref Unsafe.Add(ref _basePtr, Written);
+ offset = c;
+
+ Written++;
}
/// <summary>
@@ -116,6 +172,7 @@ namespace VNLib.Utils.Memory
/// </summary>
/// <param name="count">The number of elements to advance the writer by</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Advance(int count)
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(count, RemainingSize, nameof(Remaining));
@@ -126,6 +183,7 @@ namespace VNLib.Utils.Memory
/// Resets the writer by setting the <see cref="Written"/>
/// property to 0.
/// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset() => Written = 0;
}
}
diff --git a/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs b/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs
index f196597..9decef7 100644
--- a/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs
+++ b/lib/Utils/src/Memory/MemoryUtil.CopyUtilCore.cs
@@ -126,7 +126,7 @@ namespace VNLib.Utils.Memory
Debug.Assert(!Unsafe.IsNullRef(in srcByte), "Null source reference passed to MemmoveByRef");
Debug.Assert(!Unsafe.IsNullRef(in dstByte), "Null destination reference passed to MemmoveByRef");
- //Check for 64bit copy
+ //Check for 64bit copy (should get optimized away when sizeof(nuint == uint) aka 32bit platforms)
if(byteCount > uint.MaxValue)
{
//We need a 64bit copy strategy
@@ -135,7 +135,6 @@ namespace VNLib.Utils.Memory
//Must be supported
if(_avxCopy.Features != CopyFeatures.NotSupported)
{
- //Copy
_avxCopy.Memmove(in srcByte, ref dstByte, byteCount);
return;
}
@@ -144,7 +143,6 @@ namespace VNLib.Utils.Memory
//try reflected memove incase it supports 64bit blocks
if(_reflectedMemmove.Features != CopyFeatures.NotSupported)
{
- //Copy
_reflectedMemmove.Memmove(in srcByte, ref dstByte, byteCount);
return;
}
@@ -223,6 +221,7 @@ namespace VNLib.Utils.Memory
public CopyFeatures Features => CopyFeatures.None;
///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public void Memmove(ref readonly byte src, ref byte dst, nuint byteCount)
{
Debug.Assert(byteCount < uint.MaxValue, "Byte count must be less than uint.MaxValue and flags assumed 64bit blocks were supported");