aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/Net.Http/src/Core/ConnectionInfo.cs32
-rw-r--r--lib/Net.Http/src/Core/HttpContext.cs11
-rw-r--r--lib/Net.Http/src/Core/HttpServerProcessing.cs23
-rw-r--r--lib/Net.Http/src/Core/IConnectionContext.cs2
-rw-r--r--lib/Net.Http/src/Core/IHttpContextInformation.cs5
-rw-r--r--lib/Net.Http/src/Core/Request/HttpRequest.cs7
-rw-r--r--lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs2
-rw-r--r--lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs6
-rw-r--r--lib/Net.Http/src/Helpers/HelperTypes.cs29
-rw-r--r--lib/Net.Http/src/IConnectionInfo.cs21
-rw-r--r--lib/Net.Http/src/IHttpServer.cs5
-rw-r--r--lib/Net.Http/src/ITransportContext.cs12
-rw-r--r--lib/Net.Http/src/IWebRoot.cs10
-rw-r--r--lib/Net.Http/src/TransportSecurityInfo.cs10
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs28
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs2
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs3
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs51
-rw-r--r--lib/Plugins.Essentials/src/EventProcessor.cs19
-rw-r--r--lib/Plugins.Essentials/src/IEpProcessingOptions.cs14
-rw-r--r--lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs2
-rw-r--r--lib/Plugins.Runtime/src/PluginStackBuilder.cs6
-rw-r--r--lib/Utils/src/BitField.cs14
-rw-r--r--lib/Utils/src/Extensions/StringExtensions.cs117
-rw-r--r--lib/Utils/src/IO/VnTextReaderExtensions.cs120
-rw-r--r--lib/Utils/src/VnEncoding.cs73
-rw-r--r--lib/Utils/tests/VnEncodingTests.cs58
27 files changed, 464 insertions, 218 deletions
diff --git a/lib/Net.Http/src/Core/ConnectionInfo.cs b/lib/Net.Http/src/Core/ConnectionInfo.cs
index 8a1525c..fb22fe3 100644
--- a/lib/Net.Http/src/Core/ConnectionInfo.cs
+++ b/lib/Net.Http/src/Core/ConnectionInfo.cs
@@ -39,44 +39,70 @@ namespace VNLib.Net.Http
///<inheritdoc/>
public Uri RequestUri => Context.Request.Location;
+
///<inheritdoc/>
public string Path => RequestUri.LocalPath;
+
///<inheritdoc/>
public string? UserAgent => Context.Request.UserAgent;
+
///<inheritdoc/>
public IHeaderCollection Headers { get; private set; }
+
///<inheritdoc/>
public bool CrossOrigin { get; }
+
///<inheritdoc/>
public bool IsWebSocketRequest { get; }
+
///<inheritdoc/>
public ContentType ContentType => Context.Request.ContentType;
+
///<inheritdoc/>
public HttpMethod Method => Context.Request.Method;
+
///<inheritdoc/>
public HttpVersion ProtocolVersion => Context.Request.HttpVersion;
+
///<inheritdoc/>
- public bool IsSecure => Context.Request.EncryptionVersion != SslProtocols.None;
+ public bool IsSecure => SecurityProtocol != SslProtocols.None;
+
///<inheritdoc/>
- public SslProtocols SecurityProtocol => Context.Request.EncryptionVersion;
+ public SslProtocols SecurityProtocol
+ {
+ get
+ {
+ ref readonly TransportSecurityInfo? securityInfo = ref Context.GetSecurityInfo();
+ return securityInfo.HasValue ? securityInfo.Value.SslProtocol : SslProtocols.None;
+ }
+ }
+
///<inheritdoc/>
public Uri? Origin => Context.Request.Origin;
+
///<inheritdoc/>
public Uri? Referer => Context.Request.Referrer;
+
///<inheritdoc/>
public Tuple<long, long>? Range => Context.Request.Range;
+
///<inheritdoc/>
public IPEndPoint LocalEndpoint => Context.Request.LocalEndPoint;
+
///<inheritdoc/>
public IPEndPoint RemoteEndpoint => Context.Request.RemoteEndPoint;
+
///<inheritdoc/>
public Encoding Encoding => Context.ParentServer.Config.HttpEncoding;
+
///<inheritdoc/>
public IReadOnlyDictionary<string, string> RequestCookies => Context.Request.Cookies;
+
///<inheritdoc/>
public IReadOnlyCollection<string> Accept => Context.Request.Accept;
+
///<inheritdoc/>
- public TransportSecurityInfo? TransportSecurity => 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)
diff --git a/lib/Net.Http/src/Core/HttpContext.cs b/lib/Net.Http/src/Core/HttpContext.cs
index 3e25e70..f0cd3f8 100644
--- a/lib/Net.Http/src/Core/HttpContext.cs
+++ b/lib/Net.Http/src/Core/HttpContext.cs
@@ -114,7 +114,11 @@ namespace VNLib.Net.Http.Core
ContextFlags = new(0);
}
- public TransportSecurityInfo? GetSecurityInfo() => _ctx?.GetSecurityInfo();
+ /// <summary>
+ /// Gets a readonly reference to the transport security information
+ /// </summary>
+ /// <returns>A readonly referrence to the <see cref="TransportSecurityInfo"/> structure </returns>
+ public ref readonly TransportSecurityInfo? GetSecurityInfo() => ref _ctx!.GetSecurityInfo();
#region Context information
@@ -125,9 +129,6 @@ namespace VNLib.Net.Http.Core
HttpVersion IHttpContextInformation.CurrentVersion => Request.HttpVersion;
///<inheritdoc/>
- HttpConfig IHttpContextInformation.Config => ParentServer.Config;
-
- ///<inheritdoc/>
public ServerPreEncodedSegments EncodedSegments => ParentServer.PreEncodedSegments;
///<inheritdoc/>
@@ -164,7 +165,7 @@ namespace VNLib.Net.Http.Core
}
///<inheritdoc/>
- public Task FlushTransportAsnc()
+ public Task FlushTransportAsync()
{
return _ctx!.ConnectionStream.FlushAsync();
}
diff --git a/lib/Net.Http/src/Core/HttpServerProcessing.cs b/lib/Net.Http/src/Core/HttpServerProcessing.cs
index d3c5981..6c5fd43 100644
--- a/lib/Net.Http/src/Core/HttpServerProcessing.cs
+++ b/lib/Net.Http/src/Core/HttpServerProcessing.cs
@@ -201,8 +201,8 @@ namespace VNLib.Net.Http
//Close the response
await context.WriteResponseAsync();
- //Flush the stream before retuning
- await context.FlushTransportAsnc();
+ //Flush the stream before returning
+ await context.FlushTransportAsync();
/*
* If an alternate protocol was specified, we need to break the keepalive loop
@@ -236,6 +236,14 @@ namespace VNLib.Net.Http
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private HttpStatusCode ParseRequest(HttpContext ctx)
{
+ //Get transport security info
+ ref readonly TransportSecurityInfo? secInfo = ref ctx.GetSecurityInfo();
+
+ if (secInfo.HasValue)
+ {
+ //TODO: future support for http2 and http3 over tls
+ }
+
//Get the parse buffer
IHttpHeaderParseBuffer parseBuffer = ctx.Buffers.RequestHeaderParseBuffer;
@@ -250,7 +258,7 @@ namespace VNLib.Net.Http
Http11ParseExtensions.Http1ParseState parseState = new();
//Parse the request line
- HttpStatusCode code = ctx.Request.Http1ParseRequestLine(ref parseState, ref reader, lineBuf);
+ HttpStatusCode code = ctx.Request.Http1ParseRequestLine(ref parseState, ref reader, lineBuf, secInfo.HasValue);
if (code > 0)
{
@@ -355,15 +363,6 @@ namespace VNLib.Net.Http
return keepalive;
}
- //check for redirects
- if (root.Redirects.TryGetValue(context.Request.Location.LocalPath, out Redirect? r))
- {
- //301
- context.Redirect301(r.RedirectUrl);
- //Return keepalive
- return keepalive;
- }
-
//Check the expect header and return an early status code
if (context.Request.Expect)
{
diff --git a/lib/Net.Http/src/Core/IConnectionContext.cs b/lib/Net.Http/src/Core/IConnectionContext.cs
index fd12d74..9654964 100644
--- a/lib/Net.Http/src/Core/IConnectionContext.cs
+++ b/lib/Net.Http/src/Core/IConnectionContext.cs
@@ -55,7 +55,7 @@ namespace VNLib.Net.Http.Core
/// Flushes and pending data associated with the request to the transport
/// </summary>
/// <returns>A task that represents the flush operation</returns>
- Task FlushTransportAsnc();
+ Task FlushTransportAsync();
/// <summary>
/// Signals to the context that it will release any request specific
diff --git a/lib/Net.Http/src/Core/IHttpContextInformation.cs b/lib/Net.Http/src/Core/IHttpContextInformation.cs
index ba1566b..fbe079f 100644
--- a/lib/Net.Http/src/Core/IHttpContextInformation.cs
+++ b/lib/Net.Http/src/Core/IHttpContextInformation.cs
@@ -46,11 +46,6 @@ namespace VNLib.Net.Http.Core
HttpVersion CurrentVersion { get; }
/// <summary>
- /// The current server configuration
- /// </summary>
- HttpConfig Config { get; }
-
- /// <summary>
/// Gets the transport stream for the current connection.
/// </summary>
/// <returns>The current transport stream</returns>
diff --git a/lib/Net.Http/src/Core/Request/HttpRequest.cs b/lib/Net.Http/src/Core/Request/HttpRequest.cs
index 06168bc..cf21b19 100644
--- a/lib/Net.Http/src/Core/Request/HttpRequest.cs
+++ b/lib/Net.Http/src/Core/Request/HttpRequest.cs
@@ -25,7 +25,6 @@
using System;
using System.Net;
using System.Collections.Generic;
-using System.Security.Authentication;
using System.Runtime.CompilerServices;
using VNLib.Utils;
@@ -57,19 +56,20 @@ namespace VNLib.Net.Http.Core
public Uri? Referrer { get; set; }
internal bool KeepAlive { get; set; }
public IPEndPoint RemoteEndPoint { get; set; }
- public IPEndPoint LocalEndPoint { get; set; }
- public SslProtocols EncryptionVersion { get; set; }
+ public IPEndPoint LocalEndPoint { get; set; }
public Tuple<long, long>? Range { get; set; }
/// <summary>
/// A value indicating whether the connection contained a request entity body.
/// </summary>
public bool HasEntityBody { get; set; }
+
/// <summary>
/// A transport stream wrapper that is positioned for reading
/// the entity body from the input stream
/// </summary>
public HttpInputStream InputStream { get; }
+
/// <summary>
/// A value indicating if the client's request had an Expect-100-Continue header
/// </summary>
@@ -104,7 +104,6 @@ namespace VNLib.Net.Http.Core
{
//Set to defaults
ContentType = ContentType.NonSupported;
- EncryptionVersion = default;
Method = HttpMethod.None;
HttpVersion = HttpVersion.None;
}
diff --git a/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs b/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs
index f92a685..0da2e98 100644
--- a/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs
+++ b/lib/Net.Http/src/Core/Request/HttpRequestExtensions.cs
@@ -27,6 +27,7 @@ using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
+using System.Security.Authentication;
using System.Runtime.CompilerServices;
using VNLib.Utils.IO;
@@ -126,7 +127,6 @@ namespace VNLib.Net.Http.Core
{
server.LocalEndPoint = ctx.LocalEndPoint;
server.RemoteEndPoint = ctx.RemoteEndpoint;
- server.EncryptionVersion = ctx.SslVersion;
//Set to default http version so the response can be configured properly
server.HttpVersion = defaultHttpVersion;
}
diff --git a/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs b/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs
index c25cd12..de65e12 100644
--- a/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs
+++ b/lib/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs
@@ -27,7 +27,6 @@ using System.Net;
using System.Linq;
using System.Buffers;
using System.Collections.Generic;
-using System.Security.Authentication;
using System.Runtime.CompilerServices;
using VNLib.Utils;
@@ -64,10 +63,11 @@ namespace VNLib.Net.Http.Core
/// <param name="reader">The reader to read lines from the transport</param>
/// <param name="parseState">The HTTP1 parsing state</param>
/// <param name="lineBuf">The buffer to use when parsing the request data</param>
+ /// <param name="usingTls">True if the transport is using TLS</param>
/// <returns>0 if the request line was successfully parsed, a status code if the request could not be processed</returns>
/// <exception cref="UriFormatException"></exception>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
- public static HttpStatusCode Http1ParseRequestLine(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, Span<char> lineBuf)
+ public static HttpStatusCode Http1ParseRequestLine(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, Span<char> lineBuf, bool usingTls)
{
//Locals
ERRNO requestResult;
@@ -141,7 +141,7 @@ namespace VNLib.Net.Http.Core
parseState.Location = new()
{
//Set a default scheme
- Scheme = Request.EncryptionVersion == SslProtocols.None ? Uri.UriSchemeHttp : Uri.UriSchemeHttps,
+ Scheme = usingTls ? Uri.UriSchemeHttp : Uri.UriSchemeHttps,
};
//Need to manually parse the query string
diff --git a/lib/Net.Http/src/Helpers/HelperTypes.cs b/lib/Net.Http/src/Helpers/HelperTypes.cs
index ecdb28c..8b03dbf 100644
--- a/lib/Net.Http/src/Helpers/HelperTypes.cs
+++ b/lib/Net.Http/src/Helpers/HelperTypes.cs
@@ -172,33 +172,4 @@ namespace VNLib.Net.Http
/// </summary>
SameSite
}
-
- /// <summary>
- /// Low level 301 "hard" redirect
- /// </summary>
- public class Redirect
- {
- public readonly string Url;
- public readonly Uri RedirectUrl;
- /// <summary>
- /// Quickly redirects a url to another url before sessions are established
- /// </summary>
- /// <param name="url">Url to redirect on</param>
- /// <param name="redirecturl">Url to redirect to</param>
- public Redirect(string url, string redirecturl)
- {
- if (string.IsNullOrEmpty(url))
- {
- throw new ArgumentException($"'{nameof(url)}' cannot be null or empty.", nameof(url));
- }
-
- if (string.IsNullOrEmpty(redirecturl))
- {
- throw new ArgumentException($"'{nameof(redirecturl)}' cannot be null or empty.", nameof(redirecturl));
- }
-
- Url = url;
- RedirectUrl = new(redirecturl);
- }
- }
} \ No newline at end of file
diff --git a/lib/Net.Http/src/IConnectionInfo.cs b/lib/Net.Http/src/IConnectionInfo.cs
index 0feedd9..5664ac5 100644
--- a/lib/Net.Http/src/IConnectionInfo.cs
+++ b/lib/Net.Http/src/IConnectionInfo.cs
@@ -40,84 +40,103 @@ namespace VNLib.Net.Http
/// Full request uri of current connection
/// </summary>
Uri RequestUri { get; }
+
/// <summary>
/// Current request path. Shortcut to <seealso cref="RequestUri"/> <see cref="Uri.LocalPath"/>
/// </summary>
string Path => RequestUri.LocalPath;
+
/// <summary>
/// Current connection's user-agent header, (may be null if no user-agent header found)
/// </summary>
string? UserAgent { get; }
+
/// <summary>
/// Current connection's headers
/// </summary>
IHeaderCollection Headers { get; }
+
/// <summary>
/// A value that indicates if the connection's origin header was set and it's
/// authority segment does not match the <see cref="RequestUri"/> authority
/// segment.
/// </summary>
bool CrossOrigin { get; }
+
/// <summary>
/// Is the current connecion a websocket request
/// </summary>
bool IsWebSocketRequest { get; }
+
/// <summary>
/// Request specified content-type
/// </summary>
ContentType ContentType { get; }
+
/// <summary>
/// Current request's method
/// </summary>
HttpMethod Method { get; }
+
/// <summary>
/// The current connection's HTTP protocol version
/// </summary>
HttpVersion ProtocolVersion { get; }
+
/// <summary>
/// Is the connection using transport security?
/// </summary>
bool IsSecure { get; }
+
/// <summary>
/// The negotiated transport protocol for the current connection
/// </summary>
SslProtocols SecurityProtocol { get; }
+
/// <summary>
/// Origin header of current connection if specified, null otherwise
/// </summary>
Uri? Origin { get; }
+
/// <summary>
/// Referer header of current connection if specified, null otherwise
/// </summary>
Uri? Referer { get; }
+
/// <summary>
/// The parsed range header, or -1,-1 if the range header was not set
/// </summary>
Tuple<long, long>? Range { get; }
+
/// <summary>
/// The server endpoint that accepted the connection
/// </summary>
IPEndPoint LocalEndpoint { get; }
+
/// <summary>
/// The raw <see cref="IPEndPoint"/> of the downstream connection.
/// </summary>
IPEndPoint RemoteEndpoint { get; }
+
/// <summary>
/// The encoding type used to decode and encode character data to and from the current client
/// </summary>
Encoding Encoding { get; }
+
/// <summary>
/// A <see cref="IReadOnlyDictionary{TKey, TValue}"/> of client request cookies
/// </summary>
IReadOnlyDictionary<string, string> RequestCookies { get; }
+
/// <summary>
/// Gets an <see cref="IEnumerator{T}"/> for the parsed accept header values
/// </summary>
IReadOnlyCollection<string> Accept { get; }
+
/// <summary>
/// Gets the underlying transport security information for the current connection
/// </summary>
- TransportSecurityInfo? TransportSecurity { get; }
+ ref readonly TransportSecurityInfo? GetTransportSecurityInfo();
/// <summary>
diff --git a/lib/Net.Http/src/IHttpServer.cs b/lib/Net.Http/src/IHttpServer.cs
index ccee8fd..dea477a 100644
--- a/lib/Net.Http/src/IHttpServer.cs
+++ b/lib/Net.Http/src/IHttpServer.cs
@@ -33,11 +33,6 @@ namespace VNLib.Net.Http
public interface IHttpServer
{
/// <summary>
- /// The <see cref="HttpConfig"/> for the current server
- /// </summary>
- HttpConfig Config { get; }
-
- /// <summary>
/// Gets a value indicating whether the server is listening for connections
/// </summary>
bool Running { get; }
diff --git a/lib/Net.Http/src/ITransportContext.cs b/lib/Net.Http/src/ITransportContext.cs
index bd6ce05..622dec4 100644
--- a/lib/Net.Http/src/ITransportContext.cs
+++ b/lib/Net.Http/src/ITransportContext.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Net.Http
@@ -25,8 +25,6 @@
using System.IO;
using System.Net;
using System.Threading.Tasks;
-using System.Security.Authentication;
-
namespace VNLib.Net.Http
{
@@ -39,14 +37,12 @@ namespace VNLib.Net.Http
/// The transport network stream for application data marshaling
/// </summary>
Stream ConnectionStream { get; }
- /// <summary>
- /// The transport security layer security protocol
- /// </summary>
- SslProtocols SslVersion { get; }
+
/// <summary>
/// A copy of the local endpoint of the listening socket
/// </summary>
IPEndPoint LocalEndPoint { get; }
+
/// <summary>
/// The <see cref="IPEndPoint"/> representing the client's connection information
/// </summary>
@@ -66,6 +62,6 @@ namespace VNLib.Net.Http
/// Attemts to get the transport security details for the connection
/// </summary>
/// <returns>A the <see cref="TransportSecurityInfo"/> structure if applicable, null otherwise</returns>
- TransportSecurityInfo? GetSecurityInfo();
+ ref readonly TransportSecurityInfo? GetSecurityInfo();
}
} \ No newline at end of file
diff --git a/lib/Net.Http/src/IWebRoot.cs b/lib/Net.Http/src/IWebRoot.cs
index 9b1a9c8..d5a7e60 100644
--- a/lib/Net.Http/src/IWebRoot.cs
+++ b/lib/Net.Http/src/IWebRoot.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Net.Http
@@ -24,8 +24,6 @@
using System;
using System.Threading.Tasks;
-using System.Collections.Generic;
-
namespace VNLib.Net.Http
{
@@ -35,10 +33,12 @@ namespace VNLib.Net.Http
/// </summary>
public interface IWebRoot
{
+
/// <summary>
/// The hostname the server will listen for, and the hostname that will identify this root when a connection requests it
/// </summary>
string Hostname { get; }
+
/// <summary>
/// <para>
/// The main event handler for user code to process a request
@@ -50,9 +50,5 @@ namespace VNLib.Net.Http
/// <param name="httpEvent">An active, unprocessed event capturing the request infomration into a standard format</param>
/// <returns>A <see cref="ValueTask"/> that the processor will await until the entity has been processed</returns>
ValueTask ClientConnectedAsync(IHttpEvent httpEvent);
- /// <summary>
- /// "Low-Level" 301 redirects
- /// </summary>
- IReadOnlyDictionary<string, Redirect> Redirects { get; }
}
} \ No newline at end of file
diff --git a/lib/Net.Http/src/TransportSecurityInfo.cs b/lib/Net.Http/src/TransportSecurityInfo.cs
index 7c7a79c..2d84a4c 100644
--- a/lib/Net.Http/src/TransportSecurityInfo.cs
+++ b/lib/Net.Http/src/TransportSecurityInfo.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Net.Http
@@ -35,8 +35,14 @@ namespace VNLib.Net.Http
/// <summary>
/// Gets the transport TLS security information for the current connection
/// </summary>
- public readonly struct TransportSecurityInfo
+ public readonly record struct TransportSecurityInfo
{
+
+ /// <summary>
+ /// The transport security layer security protocol
+ /// </summary>
+ public readonly SslProtocols SslProtocol { get; init; }
+
/// <summary>
/// Gets a Boolean value that indicates whether the certificate revocation list is checked during the certificate validation process.
/// </summary>
diff --git a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs
index 5997cf9..95c6878 100644
--- a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs
+++ b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs
@@ -23,6 +23,7 @@
*/
using System;
+using System.Linq;
using System.Collections.Generic;
using VNLib.Net.Http;
@@ -36,6 +37,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
/// </summary>
public sealed class HttpServiceStackBuilder
{
+
/// <summary>
/// Initializes a new <see cref="HttpServiceStack"/> that will
/// generate servers to listen for services exposed by the
@@ -54,7 +56,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
/// </summary>
/// <param name="hostBuilder">The callback method to build virtual hosts</param>
/// <returns>The current instance for chaining</returns>
- public HttpServiceStackBuilder WithDomainBuilder(Action<ICollection<IServiceHost>> hostBuilder)
+ public HttpServiceStackBuilder WithDomain(Action<ICollection<IServiceHost>> hostBuilder)
{
_hostBuilder = hostBuilder;
return this;
@@ -83,7 +85,29 @@ namespace VNLib.Plugins.Essentials.ServiceStack
}
/// <summary>
- /// Builds the new <see cref="HttpServiceStack"/> from the configured callbacks, WITHOUT loading plugins
+ /// Configures the stack to use the built-in http server implementation
+ /// </summary>
+ /// <param name="transport">The transport builder callback function</param>
+ /// <param name="config">The http configuration structure used to initalize servers</param>
+ /// <returns>The current instance for chaining</returns>
+ public HttpServiceStackBuilder WithBuiltInHttp(Func<ServiceGroup, ITransportProvider> transport, HttpConfig config)
+ {
+ return WithHttp(sg => new HttpServer(config, transport(sg), sg.Hosts.Select(static p => p.Processor)));
+ }
+
+ /// <summary>
+ /// Configures the stack to use the built-in http server implementation
+ /// </summary>
+ /// <param name="transport">The transport builder callback function</param>
+ /// <param name="configCallback">The http configuration builder callback method</param>
+ /// <returns>The current instance for chaining</returns>
+ public HttpServiceStackBuilder WithBuiltInHttp(Func<ServiceGroup, ITransportProvider> transport, Func<ServiceGroup, HttpConfig> configCallback)
+ {
+ return WithHttp(sg => new HttpServer(configCallback(sg), transport(sg), sg.Hosts.Select(static p => p.Processor)));
+ }
+
+ /// <summary>
+ /// Builds the new <see cref="HttpServiceStack"/> from the configured callbacks
/// </summary>
/// <returns>The newly constructed <see cref="HttpServiceStack"/> that may be used to manage your http services</returns>
/// <exception cref="ArgumentNullException"></exception>
diff --git a/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs b/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs
index 5d8fbe7..2517d66 100644
--- a/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs
+++ b/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs
@@ -40,7 +40,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
IWebRoot Processor { get; }
/// <summary>
- /// The host's transport infomration
+ /// The host's transport information
/// </summary>
IHostTransportInfo TransportInfo { get; }
diff --git a/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs b/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs
index 0fe4c93..429a465 100644
--- a/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs
+++ b/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs
@@ -112,9 +112,6 @@ namespace VNLib.Plugins.Essentials.ServiceStack
{
//Dispose services
_services?.Dispose();
-
- //Dispose loader
- Plugin.Dispose();
}
diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs b/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs
index 208001a..eb26e92 100644
--- a/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs
+++ b/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs
@@ -29,7 +29,6 @@ using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;
-using System.Runtime.CompilerServices;
using VNLib.Utils;
using VNLib.Utils.Logging;
@@ -45,7 +44,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
/// </summary>
internal sealed class PluginManager : VnDisposeable, IHttpPluginManager, IPluginEventListener
{
- private readonly ConditionalWeakTable<PluginController, ManagedPlugin> _managedPlugins;
+ private readonly Dictionary<PluginController, ManagedPlugin> _managedPlugins;
private readonly ServiceDomain _dependents;
private readonly IPluginStack _stack;
@@ -71,7 +70,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack
{
_ = _stack ?? throw new InvalidOperationException("Plugin stack has not been set.");
- //Build the plugin stack
+ /*
+ * Since we own the plugin stack, it is safe to build it here.
+ * This method is not public and should not be called more than
+ * once. Otherwise it can cause issues with the plugin stack.
+ */
_stack.BuildStack();
//Register for plugin events
@@ -83,15 +86,24 @@ namespace VNLib.Plugins.Essentials.ServiceStack
//Add all wrappers to the managed plugins table
Array.ForEach(wrapper, w => _managedPlugins.Add(w.Plugin.Controller, w));
- //Init remaining controllers single-threaded
+ //Init remaining controllers single-threaded because it may mutate the table
_managedPlugins.Select(p => p.Value).TryForeach(w => InitializePlugin(w.Plugin, debugLog));
//Load stage, load all multithreaded
- Parallel.ForEach(wrapper, wp => LoadPlugin(wp.Plugin, debugLog));
+ Parallel.ForEach(_managedPlugins.Values, wp => LoadPlugin(wp.Plugin, debugLog));
debugLog.Information("Plugin loading completed");
}
+ /*
+ * Plugins are manually loaded by this manager instead of the stack shortcut extensions
+ * because I want to catch individual exceptions.
+ *
+ * I do not prefer this method as I would prefer loading is handled by the stack
+ * and the host not by this library.
+ *
+ * This will change in the future.
+ */
private void InitializePlugin(RuntimePluginLoader plugin, ILogProvider debugLog)
{
@@ -108,7 +120,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
*/
if (!plugin.Controller.Plugins.Any())
{
- debugLog.Warn("No plugin instances were exposed via {ams} assembly. This may be due to an assebmly mismatch", fileName);
+ debugLog.Warn("No plugin instances were exposed via {asm} assembly. This may be due to an assebmly mismatch", fileName);
}
}
catch (Exception ex)
@@ -117,9 +129,6 @@ namespace VNLib.Plugins.Essentials.ServiceStack
//Remove the plugin from the table
_managedPlugins.Remove(plugin.Controller);
-
- //Dispose the plugin
- plugin.Dispose();
}
}
@@ -191,12 +200,25 @@ namespace VNLib.Plugins.Essentials.ServiceStack
//Dispose all managed plugins and clear the table
_managedPlugins.TryForeach(p => p.Value.Dispose());
_managedPlugins.Clear();
+
+ //Dispose the plugin stack
+ _stack.Dispose();
}
+ /*
+ * When using a service stack an loading manually, plugins that have errors
+ * will not be captured by this instance. However when using the shortcut
+ * extensions, the events will be invoked regaldess if we loaded the plugin
+ * here.
+ */
+
void IPluginEventListener.OnPluginLoaded(PluginController controller, object? state)
{
- //Handle service events
- ManagedPlugin mp = _managedPlugins.GetValue(controller, (pc) => null!);
+ //Make sure the plugin is managed by this manager
+ if(!_managedPlugins.TryGetValue(controller, out ManagedPlugin? mp))
+ {
+ return;
+ }
//Run onload method before invoking other handlers
mp.OnPluginLoaded();
@@ -210,8 +232,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack
void IPluginEventListener.OnPluginUnloaded(PluginController controller, object? state)
{
- //Handle service events
- ManagedPlugin mp = _managedPlugins.GetValue(controller, (pc) => null!);
+ //Make sure the plugin is managed by this manager
+ if (!_managedPlugins.TryGetValue(controller, out ManagedPlugin? mp))
+ {
+ return;
+ }
//Run onload method before invoking other handlers
mp.OnPluginUnloaded();
diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs
index c4659b9..a9cd98a 100644
--- a/lib/Plugins.Essentials/src/EventProcessor.cs
+++ b/lib/Plugins.Essentials/src/EventProcessor.cs
@@ -73,9 +73,6 @@ namespace VNLib.Plugins.Essentials
/// </summary>
public abstract IEpProcessingOptions Options { get; }
- ///<inheritdoc/>
- public abstract IReadOnlyDictionary<string, Redirect> Redirects { get; }
-
/// <summary>
/// Event log provider
/// </summary>
@@ -222,18 +219,18 @@ namespace VNLib.Plugins.Essentials
while(mwNode != null)
{
//Process
- HttpMiddlewareResult result = await mwNode.ValueRef.ProcessAsync(entity);
+ args = await mwNode.ValueRef.ProcessAsync(entity);
- switch (result)
+ switch (args.Routine)
{
- //move next
- case HttpMiddlewareResult.Continue:
+ //move next if continue is returned
+ case FpRoutine.Continue:
mwNode = mwNode.Next;
break;
//Middleware completed the connection, time to exit
- case HttpMiddlewareResult.Complete:
- return;
+ default:
+ goto MwExit;
}
}
@@ -259,7 +256,7 @@ namespace VNLib.Plugins.Essentials
ProcessFile(httpEvent, in args);
return;
}
- }
+ }
//If no virtual processor handled the ws request, deny it
if (entity.Server.IsWebSocketRequest)
@@ -271,6 +268,8 @@ namespace VNLib.Plugins.Essentials
//Finally process as file
args = await RouteFileAsync(entity);
+ MwExit:
+
//Finally process the file
ProcessFile(httpEvent, in args);
}
diff --git a/lib/Plugins.Essentials/src/IEpProcessingOptions.cs b/lib/Plugins.Essentials/src/IEpProcessingOptions.cs
index 13dcd37..b9efdeb 100644
--- a/lib/Plugins.Essentials/src/IEpProcessingOptions.cs
+++ b/lib/Plugins.Essentials/src/IEpProcessingOptions.cs
@@ -27,8 +27,6 @@ using System.IO;
using System.Net;
using System.Collections.Generic;
-using VNLib.Net.Http;
-
#nullable enable
namespace VNLib.Plugins.Essentials
@@ -39,34 +37,36 @@ namespace VNLib.Plugins.Essentials
/// </summary>
public interface IEpProcessingOptions
{
+
/// <summary>
/// The name of a default file to search for within a directory if no file is specified (index.html).
/// This array should be ordered.
/// </summary>
IReadOnlyCollection<string> DefaultFiles { get; }
+
/// <summary>
/// File extensions that are denied from being read from the filesystem
/// </summary>
IReadOnlySet<string> ExcludedExtensions { get; }
+
/// <summary>
/// File attributes that must be matched for the file to be accessed
/// </summary>
FileAttributes AllowedAttributes { get; }
+
/// <summary>
/// Files that match any attribute flag set will be denied
/// </summary>
- FileAttributes DissallowedAttributes { get; }
+ FileAttributes DissallowedAttributes { get; }
+
/// <summary>
/// A table of known downstream servers/ports that can be trusted to proxy connections
/// </summary>
IReadOnlySet<IPAddress> DownStreamServers { get; }
+
/// <summary>
/// A <see cref="TimeSpan"/> for how long a connection may remain open before all operations are cancelled
/// </summary>
TimeSpan ExecutionTimeout { get; }
- /// <summary>
- /// HTTP level "hard" 301 redirects
- /// </summary>
- IReadOnlyDictionary<string, Redirect> HardRedirects { get; }
}
} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs
index a5a3949..4485c55 100644
--- a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs
+++ b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs
@@ -39,6 +39,6 @@ namespace VNLib.Plugins.Essentials.Middleware
/// </summary>
/// <param name="entity">The entity to process</param>
/// <returns>The result of the operation</returns>
- ValueTask<HttpMiddlewareResult> ProcessAsync(HttpEntity entity);
+ ValueTask<FileProcessArgs> ProcessAsync(HttpEntity entity);
}
}
diff --git a/lib/Plugins.Runtime/src/PluginStackBuilder.cs b/lib/Plugins.Runtime/src/PluginStackBuilder.cs
index 5769f3e..d05f489 100644
--- a/lib/Plugins.Runtime/src/PluginStackBuilder.cs
+++ b/lib/Plugins.Runtime/src/PluginStackBuilder.cs
@@ -49,6 +49,10 @@ namespace VNLib.Plugins.Runtime
private Func<IPluginConfig, IAssemblyLoader>? Loader;
+ /// <summary>
+ /// Shortcut constructor for easy fluent chaining.
+ /// </summary>
+ /// <returns>A new <see cref="PluginStackBuilder"/></returns>
public static PluginStackBuilder Create() => new();
/// <summary>
@@ -114,7 +118,7 @@ namespace VNLib.Plugins.Runtime
/// </summary>
/// <returns>The current builder instance for chaining</returns>
/// <exception cref="ArgumentException"></exception>
- public IPluginStack BuildStack()
+ public IPluginStack ConfigureStack()
{
_ = DiscoveryManager ?? throw new ArgumentException("You must specify a plugin discovery manager");
diff --git a/lib/Utils/src/BitField.cs b/lib/Utils/src/BitField.cs
index bc001df..8bdac4f 100644
--- a/lib/Utils/src/BitField.cs
+++ b/lib/Utils/src/BitField.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Utils
@@ -22,7 +22,6 @@
* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/.
*/
-using System;
using System.Runtime.CompilerServices;
namespace VNLib.Utils
@@ -33,22 +32,26 @@ namespace VNLib.Utils
public class BitField
{
private ulong Field;
+
/// <summary>
/// The readonly value of the <see cref="BitField"/>
/// </summary>
public ulong Value => Field;
+
/// <summary>
/// Creates a new <see cref="BitField"/> initialized to the specified value
/// </summary>
/// <param name="initial">Initial value</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public BitField(ulong initial) => Field = initial;
+
/// <summary>
/// Creates a new <see cref="BitField"/> initialized to the specified value
/// </summary>
/// <param name="initial">Initial value</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public BitField(long initial) => Field = unchecked((ulong)initial);
+
/// <summary>
/// Determines if the specified flag is set
/// </summary>
@@ -56,6 +59,7 @@ namespace VNLib.Utils
/// <returns>True if the flag(s) is currently set, false if flag is not set</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsSet(ulong mask) => (Field & mask) != 0;
+
/// <summary>
/// Determines if the specified flag is set
/// </summary>
@@ -63,6 +67,7 @@ namespace VNLib.Utils
/// <returns>True if the flag(s) is currently set, false if flag is not set</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsSet(long mask) => (Field & unchecked((ulong)mask)) != 0;
+
/// <summary>
/// Determines if the specified flag is set
/// </summary>
@@ -70,6 +75,7 @@ namespace VNLib.Utils
/// <returns>True if the flag(s) is currently set, false if flag is not set</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(ulong mask) => Field |= mask;
+
/// <summary>
/// Determines if the specified flag is set
/// </summary>
@@ -77,6 +83,7 @@ namespace VNLib.Utils
/// <returns>True if the flag(s) is currently set, false if flag is not set</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(long mask) => Field |= unchecked((ulong)mask);
+
/// <summary>
/// Sets or clears a flag(s) indentified by a mask based on the value
/// </summary>
@@ -94,18 +101,21 @@ namespace VNLib.Utils
Clear(mask);
}
}
+
/// <summary>
/// Clears the flag identified by the specified mask
/// </summary>
/// <param name="mask">The mask used to clear the given flag</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear(ulong mask) => Field &= ~mask;
+
/// <summary>
/// Clears the flag identified by the specified mask
/// </summary>
/// <param name="mask">The mask used to clear the given flag</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear(long mask) => Field &= ~unchecked((ulong)mask);
+
/// <summary>
/// Clears all flags by setting the <see cref="Field"/> property value to 0
/// </summary>
diff --git a/lib/Utils/src/Extensions/StringExtensions.cs b/lib/Utils/src/Extensions/StringExtensions.cs
index 09d6517..f211b73 100644
--- a/lib/Utils/src/Extensions/StringExtensions.cs
+++ b/lib/Utils/src/Extensions/StringExtensions.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Utils
@@ -32,6 +32,10 @@ using VNLib.Utils.Memory;
namespace VNLib.Utils.Extensions
{
+ /// <summary>
+ /// Delegate for a stateless span action
+ /// </summary>
+ /// <param name="line">The line of data to process</param>
public delegate void StatelessSpanAction(ReadOnlySpan<char> line);
/// <summary>
@@ -51,6 +55,7 @@ namespace VNLib.Utils.Extensions
{
Split(value, splitter.AsSpan(), output, options);
}
+
/// <summary>
/// Split a string based on split value and insert into the specified list
/// </summary>
@@ -66,6 +71,7 @@ namespace VNLib.Utils.Extensions
//Call the split function on the span
Split(value, cs, output, options);
}
+
/// <summary>
/// Split a string based on split value and insert into the specified list
/// </summary>
@@ -79,6 +85,7 @@ namespace VNLib.Utils.Extensions
{
Split(value.AsSpan(), splitter, output, options);
}
+
/// <summary>
/// Split a string based on split value and insert into the specified list
/// </summary>
@@ -88,13 +95,14 @@ namespace VNLib.Utils.Extensions
/// <param name="options">String split options</param>
/// <exception cref="ArgumentNullException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Split<T>(this in ReadOnlySpan<char> value, char splitter, T output, StringSplitOptions options) where T : ICollection<string>
+ public static void Split<T>(this ReadOnlySpan<char> value, char splitter, T output, StringSplitOptions options) where T : ICollection<string>
{
//Create span from char pointer
ReadOnlySpan<char> cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1);
//Call the split function on the span
- Split(in value, cs, output, options);
+ Split(value, cs, output, options);
}
+
/// <summary>
/// Split a <see cref="ReadOnlySpan{T}"/> based on split value and insert into the specified list
/// </summary>
@@ -104,13 +112,15 @@ namespace VNLib.Utils.Extensions
/// <param name="options">String split options</param>
/// <exception cref="ArgumentNullException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Split<T>(this in ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, T output, StringSplitOptions options) where T : ICollection<string>
+ public static void Split<T>(this ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, T output, StringSplitOptions options) where T : ICollection<string>
{
//Create a local function that adds the split strings to the list
static void SplitFound(ReadOnlySpan<char> split, T output) => output.Add(split.ToString());
+
//Invoke the split function with the local callback method
- Split(in value, splitter, options, SplitFound, output);
+ Split(value, splitter, options, SplitFound, output);
}
+
/// <summary>
/// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler
/// </summary>
@@ -120,11 +130,13 @@ namespace VNLib.Utils.Extensions
/// <param name="splitCb">The action to invoke when a split segment has been found</param>
/// <param name="state">The state to pass to the callback handler</param>
/// <exception cref="ArgumentNullException"></exception>
- public static void Split<T>(this in ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, StringSplitOptions options, ReadOnlySpanAction<char, T> splitCb, T state)
+ public static void Split<T>(this ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, StringSplitOptions options, ReadOnlySpanAction<char, T> splitCb, T state)
{
_ = splitCb ?? throw new ArgumentNullException(nameof(splitCb));
+
//Get span over string
ForwardOnlyReader<char> reader = new(value);
+
//No string options
if (options == 0)
{
@@ -132,41 +144,49 @@ namespace VNLib.Utils.Extensions
{
//Find index of the splitter
int start = reader.Window.IndexOf(splitter);
+
//guard
if (start == -1)
{
break;
}
+
//Trim and add it regardless of length
splitCb(reader.Window[..start], state);
+
//shift window
reader.Advance(start + splitter.Length);
} while (true);
+
//Trim remaining and add it regardless of length
splitCb(reader.Window, state);
}
//Trim but do not remove empties
- else if ((options & StringSplitOptions.RemoveEmptyEntries) == 0)
+ else if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
{
do
{
//Find index of the splitter
int start = reader.Window.IndexOf(splitter);
+
//guard
if (start == -1)
{
break;
}
+
//Trim and add it regardless of length
splitCb(reader.Window[..start].Trim(), state);
+
//shift window
reader.Advance(start + splitter.Length);
} while (true);
+
//Trim remaining and add it regardless of length
splitCb(reader.Window.Trim(), state);
}
//Remove empty entires but do not trim them
- else if ((options & StringSplitOptions.TrimEntries) == 0)
+ else if ((options & StringSplitOptions.RemoveEmptyEntries) == StringSplitOptions.RemoveEmptyEntries)
{
//Get data before splitter and trim it
ReadOnlySpan<char> data;
@@ -186,9 +206,11 @@ namespace VNLib.Utils.Extensions
{
splitCb(data, state);
}
- //shift window
- reader.Advance(start + splitter.Length);
+
+ reader.Advance(start + splitter.Length);
+
} while (true);
+
//Add if not empty
if (reader.WindowSize > 0)
{
@@ -204,23 +226,29 @@ namespace VNLib.Utils.Extensions
{
//Find index of the splitter
int start = reader.Window.IndexOf(splitter);
+
//guard
if (start == -1)
{
break;
}
+
//Get data before splitter and trim it
data = reader.Window[..start].Trim();
+
//If its not empty, then add it to the list
if (!data.IsEmpty)
{
splitCb(data, state);
}
- //shift window
+
reader.Advance(start + splitter.Length);
+
} while (true);
+
//Trim remaining
data = reader.Window.Trim();
+
//Add if not empty
if (!data.IsEmpty)
{
@@ -238,13 +266,14 @@ namespace VNLib.Utils.Extensions
/// <param name="splitCb">The action to invoke when a split segment has been found</param>
/// <param name="state"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Split<T>(this in ReadOnlySpan<char> value, char splitter, StringSplitOptions options, ReadOnlySpanAction<char, T> splitCb, T state)
+ public static void Split<T>(this ReadOnlySpan<char> value, char splitter, StringSplitOptions options, ReadOnlySpanAction<char, T> splitCb, T state)
{
//Alloc a span for char
ReadOnlySpan<char> cs = MemoryMarshal.CreateReadOnlySpan(ref splitter, 1);
//Call the split function on the span
- Split(in value, cs, options, splitCb, state);
+ Split(value, cs, options, splitCb, state);
}
+
/// <summary>
/// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler
/// </summary>
@@ -254,13 +283,14 @@ namespace VNLib.Utils.Extensions
/// <param name="splitCb">The action to invoke when a split segment has been found</param>
/// <exception cref="ArgumentNullException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Split(this in ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, StringSplitOptions options, StatelessSpanAction splitCb)
+ public static void Split(this ReadOnlySpan<char> value, ReadOnlySpan<char> splitter, StringSplitOptions options, StatelessSpanAction splitCb)
{
//Create a SpanSplitDelegate with the non-typed delegate as the state argument
static void ssplitcb(ReadOnlySpan<char> param, StatelessSpanAction callback) => callback(param);
//Call split with the new callback delegate
- Split(in value, splitter, options, ssplitcb, splitCb);
+ Split(value, splitter, options, ssplitcb, splitCb);
}
+
/// <summary>
/// Split a <see cref="ReadOnlySpan{T}"/> based on split value and pass it to the split delegate handler
/// </summary>
@@ -270,12 +300,12 @@ namespace VNLib.Utils.Extensions
/// <param name="splitCb">The action to invoke when a split segment has been found</param>
/// <exception cref="ArgumentNullException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Split(this in ReadOnlySpan<char> value, char splitter, StringSplitOptions options, StatelessSpanAction splitCb)
+ public static void Split(this ReadOnlySpan<char> value, char splitter, StringSplitOptions options, StatelessSpanAction splitCb)
{
//Create a SpanSplitDelegate with the non-typed delegate as the state argument
static void ssplitcb(ReadOnlySpan<char> param, StatelessSpanAction callback) => callback(param);
//Call split with the new callback delegate
- Split(in value, splitter, options, ssplitcb, splitCb);
+ Split(value, splitter, options, ssplitcb, splitCb);
}
/// <summary>
@@ -285,11 +315,12 @@ namespace VNLib.Utils.Extensions
/// <param name="search">Sequence to search for within the current sequence</param>
/// <returns>the index of the end of the sequenc</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int EndOf(this in ReadOnlySpan<char> data, ReadOnlySpan<char> search)
+ public static int EndOf(this ReadOnlySpan<char> data, ReadOnlySpan<char> search)
{
int index = data.IndexOf(search);
return index > -1 ? index + search.Length : -1;
}
+
/// <summary>
/// Gets the index of the end of the found character
/// </summary>
@@ -297,15 +328,18 @@ namespace VNLib.Utils.Extensions
/// <param name="search">Character to search for within the current sequence</param>
/// <returns>the index of the end of the sequence</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int EndOf(this in ReadOnlySpan<char> data, char search)
+ public static int EndOf(this ReadOnlySpan<char> data, char search)
{
int index = data.IndexOf(search);
return index > -1 ? index + 1 : -1;
}
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int IndexOf(this in Memory<byte> data, byte search) => data.Span.IndexOf(search);
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int IndexOf(this in Memory<byte> data, ReadOnlySpan<byte> search) => data.Span.IndexOf(search);
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int IndexOf(this in Memory<byte> data, ReadOnlyMemory<byte> search) => IndexOf(data, search.Span);
@@ -317,13 +351,14 @@ namespace VNLib.Utils.Extensions
/// <param name="search">The delimiting character</param>
/// <returns>The segment of data before the search character, or the entire segment if not found</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ReadOnlySpan<char> SliceBeforeParam(this in ReadOnlySpan<char> data, char search)
+ public static ReadOnlySpan<char> SliceBeforeParam(this ReadOnlySpan<char> data, char search)
{
//Find the index of the specified data
int index = data.IndexOf(search);
//Return the slice of data before the index, or an empty span if it was not found
return index > -1 ? data[..index] : data;
}
+
/// <summary>
/// Slices the current span from the begining of the segment to the first occurrance of the specified character sequence.
/// If the character sequence is not found, the entire segment is returned
@@ -332,13 +367,14 @@ namespace VNLib.Utils.Extensions
/// <param name="search">The delimiting character sequence</param>
/// <returns>The segment of data before the search character, or the entire <paramref name="data"/> if the seach sequence is not found</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ReadOnlySpan<char> SliceBeforeParam(this in ReadOnlySpan<char> data, ReadOnlySpan<char> search)
+ public static ReadOnlySpan<char> SliceBeforeParam(this ReadOnlySpan<char> data, ReadOnlySpan<char> search)
{
//Find the index of the specified data
int index = data.IndexOf(search);
//Return the slice of data before the index, or an empty span if it was not found
return index > -1 ? data[..index] : data;
}
+
/// <summary>
/// Gets the remaining segment of data after the specified search character or <see cref="ReadOnlySpan{T}.Empty"/>
/// if the search character is not found within the current segment
@@ -347,13 +383,15 @@ namespace VNLib.Utils.Extensions
/// <param name="search">The character to search for within the segment</param>
/// <returns>The segment of data after the search character or <see cref="ReadOnlySpan{T}.Empty"/> if not found</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ReadOnlySpan<char> SliceAfterParam(this in ReadOnlySpan<char> data, char search)
+ public static ReadOnlySpan<char> SliceAfterParam(this ReadOnlySpan<char> data, char search)
{
//Find the index of the specified data
- int index = EndOf(in data, search);
+ int index = EndOf(data, search);
+
//Return the slice of data after the index, or an empty span if it was not found
return index > -1 ? data[index..] : ReadOnlySpan<char>.Empty;
}
+
/// <summary>
/// Gets the remaining segment of data after the specified search sequence or <see cref="ReadOnlySpan{T}.Empty"/>
/// if the search sequence is not found within the current segment
@@ -362,42 +400,53 @@ namespace VNLib.Utils.Extensions
/// <param name="search">The sequence to search for within the segment</param>
/// <returns>The segment of data after the search sequence or <see cref="ReadOnlySpan{T}.Empty"/> if not found</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ReadOnlySpan<char> SliceAfterParam(this in ReadOnlySpan<char> data, ReadOnlySpan<char> search)
+ public static ReadOnlySpan<char> SliceAfterParam(this ReadOnlySpan<char> data, ReadOnlySpan<char> search)
{
//Find the index of the specified data
int index = EndOf(data, search);
+
//Return the slice of data after the index, or an empty span if it was not found
return index > -1 ? data[index..] : ReadOnlySpan<char>.Empty;
}
+
/// <summary>
/// Trims any leading or trailing <c>'\r'|'\n'|' '</c>(whitespace) characters from the segment
/// </summary>
/// <returns>The trimmed segment</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ReadOnlySpan<char> TrimCRLF(this in ReadOnlySpan<char> data)
+ public static ReadOnlySpan<char> TrimCRLF(this ReadOnlySpan<char> data)
{
int start = 0, end = data.Length;
+
//trim leading \r\n chars
while(start < end)
{
char t = data[start];
+
//If character \r or \n slice it off
- if (t != '\r' && t != '\n' && t != ' ') {
+ if (t != '\r' && t != '\n' && t != ' ')
+ {
break;
}
+
//Shift
start++;
}
+
//remove trailing crlf characters
while (end > start)
{
char t = data[end - 1];
+
//If character \r or \n slice it off
- if (t != '\r' && t != '\n' && t != ' ') {
+ if (t != '\r' && t != '\n' && t != ' ')
+ {
break;
}
+
end--;
}
+
return data[start..end];
}
@@ -408,7 +457,7 @@ namespace VNLib.Utils.Extensions
/// <param name="search">The sequence to search for</param>
/// <param name="replace">The sequence to write in the place of the search parameter</param>
/// <exception cref="OutOfMemoryException"></exception>
- public static int Replace(this ref Span<char> buffer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace)
+ public static int Replace(this Span<char> buffer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace)
{
ForwardOnlyWriter<char> writer = new (buffer);
writer.Replace(search, replace);
@@ -425,20 +474,25 @@ namespace VNLib.Utils.Extensions
public static void Replace(this ref ForwardOnlyWriter<char> writer, ReadOnlySpan<char> search, ReadOnlySpan<char> replace)
{
Span<char> buffer = writer.AsSpan();
+
//If the search and replacment parameters are the same length
if (search.Length == replace.Length)
{
buffer.ReplaceInPlace(search, replace);
return;
}
+
//Search and replace are not the same length
int searchLen = search.Length, start = buffer.IndexOf(search);
+
if(start == -1)
{
return;
}
+
//Replacment might be empty
writer.Reset();
+
do
{
//Append the data before the split character
@@ -449,10 +503,13 @@ namespace VNLib.Utils.Extensions
buffer = buffer[(start + searchLen)..];
//search for next index
start = buffer.IndexOf(search);
+
} while (start > -1);
+
//Write remaining data
writer.Append(replace);
}
+
/// <summary>
/// Replaces very ocurrance of character sequence within a buffer with another sequence of the same length
/// </summary>
@@ -466,13 +523,17 @@ namespace VNLib.Utils.Extensions
{
throw new ArgumentException("Search parameter and replacment parameter must be the same length");
}
+
int start = buffer.IndexOf(search);
+
while(start > -1)
{
//Shift the buffer to the begining of the search parameter
buffer = buffer[start..];
+
//Overwite the search parameter
replace.CopyTo(buffer);
+
//Search for next index of the search character
start = buffer.IndexOf(search);
}
diff --git a/lib/Utils/src/IO/VnTextReaderExtensions.cs b/lib/Utils/src/IO/VnTextReaderExtensions.cs
index 119461b..9ca5ae5 100644
--- a/lib/Utils/src/IO/VnTextReaderExtensions.cs
+++ b/lib/Utils/src/IO/VnTextReaderExtensions.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Utils
@@ -52,8 +52,9 @@ namespace VNLib.Utils.IO
/// <remarks>Allows reading lines of data from the stream without allocations</remarks>
public static ERRNO ReadLine<T>(this ref T reader, Span<char> charBuffer) where T:struct, IVnTextReader
{
- return readLine(ref reader, charBuffer);
+ return ReadLineInternal(ref reader, charBuffer);
}
+
/// <summary>
/// Attempts to read a line from the stream and store it in the specified buffer
/// </summary>
@@ -65,7 +66,7 @@ namespace VNLib.Utils.IO
/// <remarks>Allows reading lines of data from the stream without allocations</remarks>
public static ERRNO ReadLine<T>(this T reader, Span<char> charBuffer) where T : class, IVnTextReader
{
- return readLine(ref reader, charBuffer);
+ return ReadLineInternal(ref reader, charBuffer);
}
/// <summary>
@@ -80,6 +81,7 @@ namespace VNLib.Utils.IO
{
return reader.ReadRemaining(buffer.AsSpan(offset, count));
}
+
/// <summary>
/// Fill a buffer with reamining buffered data
/// </summary>
@@ -103,8 +105,9 @@ namespace VNLib.Utils.IO
/// <remarks>You should use the <see cref="IVnTextReader.Available"/> property to know how much remaining data is buffered</remarks>
public static int ReadRemaining<T>(this ref T reader, Span<byte> buffer) where T : struct, IVnTextReader
{
- return readRemaining(ref reader, buffer);
+ return ReadRemainingInternal(ref reader, buffer);
}
+
/// <summary>
/// Fill a buffer with reamining buffered data, up to
/// the size of the supplied buffer
@@ -115,10 +118,10 @@ namespace VNLib.Utils.IO
/// <remarks>You should use the <see cref="IVnTextReader.Available"/> property to know how much remaining data is buffered</remarks>
public static int ReadRemaining<T>(this T reader, Span<byte> buffer) where T : class, IVnTextReader
{
- return readRemaining(ref reader, buffer);
+ return ReadRemainingInternal(ref reader, buffer);
}
- private static ERRNO readLine<T>(ref T reader, Span<char> chars) where T: IVnTextReader
+ private static ERRNO ReadLineInternal<T>(ref T reader, Span<char> chars) where T: IVnTextReader
{
/*
* I am aware of a potential bug, the line decoding process
@@ -129,68 +132,32 @@ namespace VNLib.Utils.IO
* I dont expect this to be an issue unless there is a bug within the specified
* encoder implementation
*/
- ReadOnlySpan<byte> LineTermination = reader.LineTermination.Span;
+
+ int result = 0;
+
//If buffered data is available, check for line termination
- if (reader.Available > 0)
+ if (reader.Available > 0 && TryReadLine(ref reader, chars, ref result))
{
- //Get current buffer window
- ReadOnlySpan<byte> bytes = reader.BufferedDataWindow;
- //search for line termination in current buffer
- int term = bytes.IndexOf(LineTermination);
- //Termination found in buffer window
- if (term > -1)
- {
- //Capture the line from the begining of the window to the termination
- ReadOnlySpan<byte> line = bytes[..term];
- //Get the number ot chars
- int charCount = reader.Encoding.GetCharCount(line);
- //See if the buffer is large enough
- if (bytes.Length < charCount)
- {
- return E_BUFFER_TOO_SMALL;
- }
- //Use the decoder to convert the data
- _ = reader.Encoding.GetChars(line, chars);
- //Shift the window to the end of the line (excluding the termination, regardless of the conversion result)
- reader.Advance(term + LineTermination.Length);
- //Return the number of characters
- return charCount;
- }
- //Termination not found but there may be more data waiting
+ return result;
}
+
//Compact the buffer window and make sure it was compacted so there is room to fill the buffer
- if (reader.CompactBufferWindow())
+ if (reader.CompactBufferWindow() > 0)
{
//There is room, so buffer more data
reader.FillBuffer();
+
//Check again to see if more data is buffered
if (reader.Available <= 0)
{
//No data avialable
return 0;
}
- //Get current buffer window
- ReadOnlySpan<byte> bytes = reader.BufferedDataWindow;
- //search for line termination in current buffer
- int term = bytes.IndexOf(LineTermination);
- //Termination found in buffer window
- if (term > -1)
+
+ //Try to read the line again after refill
+ if (TryReadLine(ref reader, chars, ref result))
{
- //Capture the line from the begining of the window to the termination
- ReadOnlySpan<byte> line = bytes[..term];
- //Get the number ot chars
- int charCount = reader.Encoding.GetCharCount(line);
- //See if the buffer is large enough
- if (bytes.Length < charCount)
- {
- return E_BUFFER_TOO_SMALL;
- }
- //Use the decoder to convert the data
- _ = reader.Encoding.GetChars(line, chars);
- //Shift the window to the end of the line (excluding the termination, regardless of the conversion result)
- reader.Advance(term + LineTermination.Length);
- //Return the number of characters
- return charCount;
+ return result;
}
}
@@ -201,20 +168,63 @@ namespace VNLib.Utils.IO
throw new OutOfMemoryException("The line was not found within the current buffer, cannot continue");
#pragma warning restore CA2201 // Do not raise reserved exception types
}
+
+ private static bool TryReadLine<T>(ref T reader, Span<char> chars, ref int result) where T: IVnTextReader
+ {
+ ReadOnlySpan<byte> LineTermination = reader.LineTermination.Span;
+
+ //Get current buffer window
+ ReadOnlySpan<byte> bytes = reader.BufferedDataWindow;
+
+ //search for line termination in current buffer
+ int term = bytes.IndexOf(LineTermination);
+
+ //Termination found in buffer window
+ if (term > -1)
+ {
+ //Capture the line from the begining of the window to the termination
+ ReadOnlySpan<byte> line = bytes[..term];
+
+ //Get the number ot chars
+ result = reader.Encoding.GetCharCount(line);
+
+ //See if the buffer is large enough
+ if (bytes.Length < result)
+ {
+ result = E_BUFFER_TOO_SMALL;
+ return true;
+ }
+
+ //Use the decoder to convert the data
+ _ = reader.Encoding.GetChars(line, chars);
+
+ //Shift the window to the end of the line (excluding the termination, regardless of the conversion result)
+ reader.Advance(term + LineTermination.Length);
+
+ //Return the number of characters
+ return true;
+ }
+
+ return false;
+ }
- private static int readRemaining<T>(ref T reader, Span<byte> buffer) where T: IVnTextReader
+ private static int ReadRemainingInternal<T>(ref T reader, Span<byte> buffer) where T: IVnTextReader
{
//guard for empty buffer
if (buffer.Length == 0 || reader.Available == 0)
{
return 0;
}
+
//get the remaining bytes in the reader
Span<byte> remaining = reader.BufferedDataWindow;
+
//Calculate the number of bytes to copy
int canCopy = Math.Min(remaining.Length, buffer.Length);
+
//Copy remaining bytes to buffer
remaining[..canCopy].CopyTo(buffer);
+
//Shift the window by the number of bytes copied
reader.Advance(canCopy);
return canCopy;
diff --git a/lib/Utils/src/VnEncoding.cs b/lib/Utils/src/VnEncoding.cs
index 9a50a50..b8f18bd 100644
--- a/lib/Utils/src/VnEncoding.cs
+++ b/lib/Utils/src/VnEncoding.cs
@@ -45,6 +45,7 @@ namespace VNLib.Utils
/// </summary>
public static class VnEncoding
{
+
/// <summary>
/// Encodes a <see cref="ReadOnlySpan{T}"/> with the specified <see cref="Encoding"/> to a <see cref="VnMemoryStream"/> that must be disposed by the user
/// </summary>
@@ -90,6 +91,7 @@ namespace VNLib.Utils
//Return default if null
return data == null || data.Length == 0 ? ValueTask.FromResult<T?>(default) : JsonSerializer.DeserializeAsync<T>(data, options, cancellationToken);
}
+
/// <summary>
/// Attempts to deserialze a json object from a stream of UTF8 data
/// </summary>
@@ -105,6 +107,7 @@ namespace VNLib.Utils
//Return default if null
return data == null || data.Length == 0 ? ValueTask.FromResult<object?>(default) : JsonSerializer.DeserializeAsync(data, type, options, cancellationToken);
}
+
/// <summary>
/// Attempts to serialize the object to json and write the encoded data to the stream
/// </summary>
@@ -538,6 +541,8 @@ namespace VNLib.Utils
#region percent encoding
+ private const int MAX_STACKALLOC = 1024;
+
private static readonly ReadOnlyMemory<byte> HexToUtf8Pos = new byte[16]
{
0x30, //0
@@ -572,9 +577,10 @@ namespace VNLib.Utils
* For every illegal character, the percent encoding adds 3 bytes of
* entropy. So a single byte will be replaced by 3, so adding
* 2 bytes for every illegal character plus the length of the
- * intial buffer, we get the size of the buffer needed to
+ * intial buffer, we get the exact size of the buffer needed to
* percent encode.
*/
+
int count = 0, len = utf8Bytes.Length;
fixed (byte* utfBase = &MemoryMarshal.GetReference(utf8Bytes))
{
@@ -658,29 +664,29 @@ namespace VNLib.Utils
{
int outPos = 0, len = utf8Encoded.Length;
ReadOnlySpan<byte> lookupTable = HexToUtf8Pos.Span;
-
+
for (int i = 0; i < len; i++)
{
byte value = utf8Encoded[i];
//Begining of percent encoding character
- if(value == 0x25)
+ if (value == 0x25)
{
//Calculate the base16 multiplier from the upper half of the
int multiplier = lookupTable.IndexOf(utf8Encoded[i + 1]);
-
+
//get the base16 lower half to add
int lower = lookupTable.IndexOf(utf8Encoded[i + 2]);
-
+
//Check format
- if(multiplier < 0 || lower < 0)
+ if (multiplier < 0 || lower < 0)
{
throw new FormatException($"Encoded buffer contains invalid hexadecimal characters following the % character at position {i}");
}
-
+
//Calculate the new value, shift multiplier to the upper 4 bits, then mask + or the lower 4 bits
value = (byte)(((byte)(multiplier << 4)) | ((byte)lower & 0x0f));
-
+
//Advance the encoded index by the two consumed chars
i += 2;
}
@@ -690,6 +696,56 @@ namespace VNLib.Utils
return outPos;
}
+ /// <summary>
+ /// Encodes the utf8 encoded character buffer to its percent/hex encoded utf8
+ /// character representation and returns the encoded string
+ /// </summary>
+ /// <param name="utf8Bytes">The bytes to encode</param>
+ /// <param name="allowedChars">A collection of allowed characters that will not be encoded</param>
+ /// <returns>The percent encoded string</returns>
+ /// <exception cref="FormatException"></exception>
+ public static string PercentEncode(ReadOnlySpan<byte> utf8Bytes, ReadOnlySpan<byte> allowedChars = default)
+ {
+ /*
+ * I cannot avoid the allocation of a binary buffer without doing some sketchy
+ * byte -> char cast on the string.create method. Which would also require object
+ * allocation for state data, and since spans are used, we cannot cross that
+ * callback boundry anyway.
+ */
+
+ int bufferSize = PercentEncodeCalcBufferSize(utf8Bytes, allowedChars);
+
+ //use stackalloc if the buffer is small enough
+ if (bufferSize <= MAX_STACKALLOC)
+ {
+ //stack alloc output buffer
+ Span<byte> output = stackalloc byte[bufferSize];
+
+ ERRNO encoded = PercentEncode(utf8Bytes, output, allowedChars);
+
+ if(encoded <= 0)
+ {
+ throw new FormatException("Failed to percent encode the input data");
+ }
+
+ return Encoding.UTF8.GetString(output);
+ }
+ else
+ {
+ //Alloc heap buffer
+ using UnsafeMemoryHandle<byte> handle = MemoryUtil.UnsafeAllocNearestPage(bufferSize);
+
+ ERRNO encoded = PercentEncode(utf8Bytes, handle.Span, allowedChars);
+
+ if (encoded <= 0)
+ {
+ throw new FormatException("Failed to percent encode the input data");
+ }
+
+ return Encoding.UTF8.GetString(handle.AsSpan(0, encoded));
+ }
+ }
+
#endregion
#region Base64
@@ -767,6 +823,7 @@ namespace VNLib.Utils
}
}
}
+
/// <summary>
/// Converts a base64url encoded utf8 encoded binary buffer to
/// its base64 encoded version
diff --git a/lib/Utils/tests/VnEncodingTests.cs b/lib/Utils/tests/VnEncodingTests.cs
index f1ef5f4..f2b5e85 100644
--- a/lib/Utils/tests/VnEncodingTests.cs
+++ b/lib/Utils/tests/VnEncodingTests.cs
@@ -30,6 +30,7 @@ using System.Buffers.Text;
using System.Security.Cryptography;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Diagnostics;
namespace VNLib.Utils.Tests
{
@@ -92,6 +93,61 @@ namespace VNLib.Utils.Tests
}
-
+ [TestMethod()]
+ public void PercentEncodeTest()
+ {
+ const string urlEnoded = "https%3A%2F%2Fwww.google.com%2Fsearch%3Fq%3Dtest%26oq%3Dtest%26aqs%3Dchrome..69i57j0l7.1001j0j7%26sourceid%3Dchrome%26ie%3DUTF-8";
+ const string urlDecoded = "https://www.google.com/search?q=test&oq=test&aqs=chrome..69i57j0l7.1001j0j7&sourceid=chrome&ie=UTF-8";
+
+ //We need to allow the '.' character to be encoded
+ ReadOnlySpan<byte> allowedChars = Encoding.UTF8.GetBytes(".");
+
+
+ /*
+ * Test that the url encoded string is the same as the percent encoded string
+ */
+
+ ReadOnlySpan<byte> utf8Encoded = Encoding.UTF8.GetBytes(urlDecoded);
+
+ string percentEncoded = VnEncoding.PercentEncode(utf8Encoded, allowedChars);
+
+ Assert.IsTrue(percentEncoded.Equals(urlEnoded, StringComparison.Ordinal));
+
+ /*
+ * Test decoding the percent encoded string
+ */
+
+ ReadOnlySpan<byte> percentEncodedUtf8 = Encoding.UTF8.GetBytes(urlEnoded);
+
+ byte[] outBuffer = new byte[percentEncodedUtf8.Length];
+
+ ERRNO decoded = VnEncoding.PercentDecode(percentEncodedUtf8, outBuffer);
+
+ //Make sure result is valid
+ Debug.Assert(decoded > 0);
+
+ string decodedString = Encoding.UTF8.GetString(outBuffer, 0, decoded);
+
+ Assert.IsTrue(decodedString.Equals(urlDecoded, StringComparison.Ordinal));
+ }
+
+ [TestMethod()]
+ public void Base32BasicEncodeDecodeTest()
+ {
+ const string base32Encoded = "JBSWY3DPEBLW64TMMQQQ====";
+ const string base32Decoded = "Hello World!";
+ byte[] rawBytes = Encoding.UTF8.GetBytes(base32Decoded);
+
+ //Recover bytes from base32 encoded string
+ byte[]? fromString = VnEncoding.FromBase32String(base32Encoded);
+ Assert.IsNotNull(fromString);
+
+ //Test that the decoded bytes are the same as the raw bytes
+ Assert.IsTrue(rawBytes.SequenceEqual(fromString));
+
+ //Test that the encoded string is the same as the base32 encoded string
+ string toString = VnEncoding.ToBase32String(rawBytes, true);
+ Assert.IsTrue(toString.Equals(base32Encoded, StringComparison.Ordinal));
+ }
}
} \ No newline at end of file