diff --git a/src/Entity/HttpHeaderCollection.cs b/src/Entity/HttpHeaderCollection.cs index 75033e8..3b5d2a0 100644 --- a/src/Entity/HttpHeaderCollection.cs +++ b/src/Entity/HttpHeaderCollection.cs @@ -9,6 +9,7 @@ using Sisk.Core.Http; using System.Collections.Specialized; +using System.Text; using Header = Sisk.Core.Internal.HttpKnownHeaderNames; namespace Sisk.Core.Entity; @@ -34,6 +35,17 @@ public HttpHeaderCollection(NameValueCollection headers) : base(headers) { } + /// + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + foreach (string key in this.Keys) + { + sb.AppendLine($"{key}: {this[key]}"); + } + return sb.ToString(); + } + /// /// Gets or sets the value of the HTTP Accept header. /// diff --git a/src/Entity/MultipartFormCollection.cs b/src/Entity/MultipartFormCollection.cs index 10f8e40..d5cba55 100644 --- a/src/Entity/MultipartFormCollection.cs +++ b/src/Entity/MultipartFormCollection.cs @@ -9,6 +9,7 @@ using System.Collections; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace Sisk.Core.Entity; @@ -16,7 +17,7 @@ namespace Sisk.Core.Entity; /// /// Represents an class which hosts an multipart form data contents. /// -public sealed class MultipartFormCollection : IEnumerable, IReadOnlyList, IReadOnlyCollection +public sealed class MultipartFormCollection : IReadOnlyList, IReadOnlyDictionary { private readonly IList _items; @@ -61,23 +62,83 @@ public StringValue GetStringValue(string name) public MultipartObject this[int index] => ((IReadOnlyList)_items)[index]; /// + /// + public MultipartObject this[string name] => GetItem(name, false) ?? throw new KeyNotFoundException(); + /// public int Count => ((IReadOnlyCollection)_items).Count; - /// + + /// + public IEnumerable Keys + { + get + { + for (int i = 0; i < _items.Count; i++) + { + MultipartObject? item = _items[i]; + yield return item.Name; + } + } + } + + + /// + public IEnumerable Values + { + get + { + for (int i = 0; i < _items.Count; i++) + { + MultipartObject? item = _items[i]; + yield return item; + } + } + } + /// public IEnumerator GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } - /// /// IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } + /// + public bool ContainsKey(string key) + { + return _items.Any(i => i.Name.CompareTo(key) == 0); + } + + /// + public bool TryGetValue(string key, [MaybeNullWhen(false)] out MultipartObject value) + { + var i = _items.FirstOrDefault(item => item.Name.CompareTo(key) == 0); + if (i is null) + { + value = default; + return false; + } + else + { + value = i; + return true; + } + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + for (int i = 0; i < _items.Count; i++) + { + MultipartObject? item = _items[i]; + yield return new KeyValuePair(item.Name, item); + } + } + /// /// public static implicit operator MultipartObject[](MultipartFormCollection t) diff --git a/src/Entity/MultipartFormReader.cs b/src/Entity/MultipartFormReader.cs new file mode 100644 index 0000000..840ae28 --- /dev/null +++ b/src/Entity/MultipartFormReader.cs @@ -0,0 +1,203 @@ +// The Sisk Framework source code +// Copyright (c) 2023 PROJECT PRINCIPIUM +// +// The code below is licensed under the MIT license as +// of the date of its publication, available at +// +// File name: MultipartFormReader.cs +// Repository: https://github.com/sisk-http/core + +using Sisk.Core.Internal; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Sisk.Core.Entity; + +internal sealed class MultipartFormReader +{ + byte[] boundaryBytes; + byte[] bytes; + byte[] nlbytes; + int position = 0; + Encoding encoder; + bool debugEnabled; + + public MultipartFormReader(byte[] inputBytes, byte[] boundaryBytes, Encoding baseEncoding, bool debugEnabled) + { + this.boundaryBytes = boundaryBytes; + this.encoder = baseEncoding; + this.bytes = inputBytes; + this.position = 0; + this.nlbytes = baseEncoding.GetBytes("\r\n"); + this.debugEnabled = debugEnabled; + } + + void ThrowDataException(string message) + { + if (debugEnabled) + { + throw new InvalidDataException(SR.Format(SR.MultipartFormReader_InvalidData, position, message)); + } + } + + bool CanRead { get => position < bytes.Length; } + + int ReadByte() + { + if (CanRead) + return bytes[position++]; + + return -1; + } + + void ReadNewLine() + { + position += nlbytes.Length; + } + + int Read(Span buffer) + { + int read = 0; + for (int i = 0; i < buffer.Length; i++) + { + if (ReadByte() is > 0 and int b) + { + buffer[read++] = (byte)b; + } + else break; + } + return read; + } + + public MultipartObject[] Read() + { + List objects = new List(); + while (this.CanRead) + { + ReadNextBoundary(); + NameValueCollection headers = ReadHeaders(); + + if (!CanRead) + break; + + byte[] content = ReadContent().ToArray(); + + ReadNewLine(); + + string? contentDisposition = headers[HttpKnownHeaderNames.ContentDisposition]; + if (contentDisposition is null) + { + ThrowDataException("The Content-Disposition header is empty or missing."); + continue; + } + + NameValueCollection cdispositionValues = CookieParser.ParseCookieString(contentDisposition); + string? formItemName = CookieParser.RemoveValueQuotes(cdispositionValues["name"]); + string? formFilename = CookieParser.RemoveValueQuotes(cdispositionValues["filename"]); + + if (string.IsNullOrEmpty(formItemName)) + { + ThrowDataException("The Content-Disposition \"name\" parameter is empty or missing."); + continue; + } + + MultipartObject resultObj = new MultipartObject(headers, formFilename, formItemName, content, encoder); + + objects.Add(resultObj); + } + + return objects.ToArray(); + } + + string ReadLine() + { + Span line = stackalloc byte[2048]; + int read, + n = 0, + lnbytelen = nlbytes.Length; + + while ((read = ReadByte()) > 0) + { + if (n == line.Length) + { + ThrowDataException($"Header line was too long (> {line.Length} bytes allocated)."); + break; + } + + line[n++] = (byte)read; + + if (n >= lnbytelen) + { + if (line[(n - lnbytelen)..n].SequenceEqual(nlbytes)) + { + break; + } + } + } + + return encoder.GetString(line[0..n]); + } + + Span ReadContent() + { + int boundaryLen = boundaryBytes.Length; + int istart = position; + + while (CanRead) + { + position++; + + if ((position - istart) > boundaryLen) + { + if (bytes[(position - boundaryLen)..position].SequenceEqual(boundaryBytes)) + { + break; + } + } + } + + position -= boundaryLen + nlbytes.Length + 2 /* the boundary "--" construct */; + + return bytes[istart..position]; + } + + NameValueCollection ReadHeaders() + { + NameValueCollection headers = new NameValueCollection(); + string? line; + while (!string.IsNullOrEmpty(line = ReadLine())) + { + int sepIndex = line.IndexOf(':'); + if (sepIndex == -1) + break; + + string hname = line.Substring(0, sepIndex); + string hvalue = line.Substring(sepIndex + 1).Trim(); + + headers.Add(hname, hvalue); + } + + return headers; + } + + unsafe void ReadNextBoundary() + { + Span boundaryBlock = stackalloc byte[boundaryBytes.Length + 2]; + int nextLine = Read(boundaryBlock); + + ReadNewLine(); + + if (nextLine != boundaryBlock.Length) + { + ThrowDataException($"Boundary expected at byte {position}."); + } + if (!boundaryBlock[2..].SequenceEqual(boundaryBytes)) + { + ThrowDataException($"The provided boundary string does not match the request boundary string."); + } + } +} \ No newline at end of file diff --git a/src/Entity/MultipartObject.cs b/src/Entity/MultipartObject.cs index 28cb30c..947957a 100644 --- a/src/Entity/MultipartObject.cs +++ b/src/Entity/MultipartObject.cs @@ -22,50 +22,56 @@ public sealed class MultipartObject private readonly Encoding _baseEncoding; /// - /// The multipart form data object headers. + /// Gets this headers. /// - public NameValueCollection Headers { get; private set; } + public HttpHeaderCollection Headers { get; private set; } /// - /// The name of the file provided by Multipart form data. Null is returned if the object is not a file. + /// Gets this provided file name. If this object ins't disposing a file, + /// nothing is returned. /// public string? Filename { get; private set; } /// - /// The multipart form data object field name. + /// Gets this field name. /// public string Name { get; private set; } /// - /// The multipart form data content bytes. + /// Gets this form data content in bytes. /// public byte[] ContentBytes { get; private set; } /// - /// The multipart form data content length. + /// Gets this form data content length in byte count. /// public int ContentLength { get; private set; } + /// + /// Gets an booolean indicating if this has contents or not. + /// + public bool HasContents { get => ContentLength > 0; } + /// /// Reads the content bytes with the given encoder. /// - public string? ReadContentAsString(Encoding encoder) + public string ReadContentAsString(Encoding encoder) { if (ContentLength == 0) - return null; + return string.Empty; return encoder.GetString(ContentBytes); } /// /// Reads the content bytes using the HTTP request content-encoding. /// - public string? ReadContentAsString() + public string ReadContentAsString() { return ReadContentAsString(_baseEncoding); } /// - /// Determine the image format based in the file header for each image content type. + /// Determines the image format based in the file header for each image content type. /// public MultipartObjectCommonFormat GetCommonFileFormat() { @@ -79,7 +85,7 @@ public MultipartObjectCommonFormat GetCommonFileFormat() { return MultipartObjectCommonFormat.PNG; } - } + } if (byteLen >= 4) { Span len4 = ContentBytes.AsSpan(0, 4); @@ -124,7 +130,7 @@ public MultipartObjectCommonFormat GetCommonFileFormat() internal MultipartObject(NameValueCollection headers, string? filename, string name, byte[]? body, Encoding encoding) { - Headers = headers; + Headers = new HttpHeaderCollection(headers); Filename = filename; Name = name; ContentBytes = body ?? Array.Empty(); @@ -145,8 +151,9 @@ internal static MultipartFormCollection ParseMultipartObjects(HttpRequest req) string[] contentTypePieces = contentType.Split(';'); string? boundary = null; - foreach (string obj in contentTypePieces) + for (int i = 0; i < contentTypePieces.Length; i++) { + string obj = contentTypePieces[i]; string[] kv = obj.Split("="); if (kv.Length != 2) { continue; } @@ -163,6 +170,14 @@ internal static MultipartFormCollection ParseMultipartObjects(HttpRequest req) byte[] boundaryBytes = Encoding.UTF8.GetBytes(boundary); + if (req.baseServer.ServerConfiguration.Flags.EnableNewMultipartFormReader == true) + { + MultipartFormReader reader = new MultipartFormReader(req.RawBody, boundaryBytes, req.RequestEncoding, req.baseServer.ServerConfiguration.ThrowExceptions); + var objects = reader.Read(); + + return new MultipartFormCollection(objects); + } + ///////// // https://stackoverflow.com/questions/9755090/split-a-byte-array-at-a-delimiter byte[][] Separate(byte[] source, byte[] separator) @@ -271,8 +286,9 @@ bool Equals(byte[] source, byte[] separator, int index) string? fieldName = null; string? fieldFilename = null; - foreach (string valueAttribute in val) + for (int k = 0; k < val.Length; k++) { + string valueAttribute = val[k]; string[] valAttributeParts = valueAttribute.Trim().Split("="); if (valAttributeParts.Length != 2) continue; diff --git a/src/Entity/StringValue.cs b/src/Entity/StringValue.cs index fff4bc1..0d2eed4 100644 --- a/src/Entity/StringValue.cs +++ b/src/Entity/StringValue.cs @@ -71,6 +71,18 @@ internal StringValue(string name, string type, string? data) return this; } + /// + /// Gets an object representation from this , parsing the current string expression into an value of + /// . This method will throw an if + /// the value stored in this instance is null. + /// + /// The type. + public TEnum GetEnum() where TEnum : struct, Enum + { + ThrowIfNull(); + return Enum.Parse(_ref!, true); + } + /// /// Gets a non-null string from this . This method will throw an if /// the value stored in this instance is null. diff --git a/src/Http/HttpRequest.cs b/src/Http/HttpRequest.cs index 02aec7b..1982129 100644 --- a/src/Http/HttpRequest.cs +++ b/src/Http/HttpRequest.cs @@ -25,6 +25,7 @@ namespace Sisk.Core.Http public class HttpRequestException : Exception { internal HttpRequestException(string message) : base(message) { } + internal HttpRequestException(string message, Exception? innerException) : base(message, innerException) { } } /// @@ -167,41 +168,8 @@ public NameValueCollection Cookies { if (cookies is null) { - cookies = new NameValueCollection(); string? cookieHeader = listenerRequest.Headers[HttpKnownHeaderNames.Cookie]; - - if (!string.IsNullOrWhiteSpace(cookieHeader)) - { - string[] cookiePairs = cookieHeader.Split(';'); - - for (int i = 0; i < cookiePairs.Length; i++) - { - string cookieExpression = cookiePairs[i]; - - if (string.IsNullOrWhiteSpace(cookieExpression)) - continue; - - int eqPos = cookieExpression.IndexOf('='); - if (eqPos < 0) - { - cookies[cookieExpression] = ""; - continue; - } - else - { - string cookieName = cookieExpression.Substring(0, eqPos).Trim(); - string cookieValue = cookieExpression.Substring(eqPos + 1).Trim(); - - if (string.IsNullOrWhiteSpace(cookieName)) - { - // provided an name/value pair, but no name - continue; - } - - cookies[cookieName] = WebUtility.UrlDecode(cookieValue); - } - } - } + cookies = CookieParser.ParseCookieString(cookieHeader); } return cookies; @@ -355,7 +323,14 @@ public IPAddress RemoteAddress /// public MultipartFormCollection GetMultipartFormContent() { - return MultipartObject.ParseMultipartObjects(this); + try + { + return MultipartObject.ParseMultipartObjects(this); + } + catch (Exception ex) + { + throw new HttpRequestException(SR.Format(SR.MultipartFormReader_Exception, ex.Message), ex); + } } /// @@ -402,7 +377,14 @@ public string GetRawHttpRequest(bool includeBody = true, bool appendExtraInfo = // Content if (includeBody) { - sb.Append(Body); + if (Body.Length < 8 * HttpServer.UnitKb) + { + sb.Append(Body); + } + else + { + sb.Append($"| ({HttpServer.HumanReadableSize(Body.Length)} bytes)"); + } } return sb.ToString(); diff --git a/src/Http/HttpResponse.cs b/src/Http/HttpResponse.cs index ddda57f..41928c9 100644 --- a/src/Http/HttpResponse.cs +++ b/src/Http/HttpResponse.cs @@ -119,7 +119,19 @@ public string GetRawHttpResponse(bool includeBody = true) if (includeBody && Content is not StreamContent) { - sb.Append(Content?.ReadAsStringAsync().Result); + string? s = Content?.ReadAsStringAsync().Result; + + if (s is not null) + { + if (s.Length < 8 * HttpServer.UnitKb) + { + sb.Append(s); + } + else + { + sb.Append($"| ({HttpServer.HumanReadableSize(s.Length)} bytes)"); + } + } } return sb.ToString(); diff --git a/src/Http/HttpServerFlags.cs b/src/Http/HttpServerFlags.cs index 385a6d9..6f59afd 100644 --- a/src/Http/HttpServerFlags.cs +++ b/src/Http/HttpServerFlags.cs @@ -126,6 +126,15 @@ public sealed class HttpServerFlags /// public TimeSpan IdleConnectionTimeout = TimeSpan.FromSeconds(120); + /// + /// Determines if the new span-based multipart form reader should be used. This is an experimental + /// feature and may not be stable for production usage. + /// + /// Default value: false + /// + /// + public bool EnableNewMultipartFormReader = false; + /// /// Creates an new instance with default flags values. /// diff --git a/src/Http/HttpServer__Core.cs b/src/Http/HttpServer__Core.cs index e19c211..19298d5 100644 --- a/src/Http/HttpServer__Core.cs +++ b/src/Http/HttpServer__Core.cs @@ -19,13 +19,13 @@ namespace Sisk.Core.Http; public partial class HttpServer { - const long UnitKb = 1024; - const long UnitMb = UnitKb * 1024; - const long UnitGb = UnitMb * 1024; - const long UnitTb = UnitGb * 1024; + internal const long UnitKb = 1024; + internal const long UnitMb = UnitKb * 1024; + internal const long UnitGb = UnitMb * 1024; + internal const long UnitTb = UnitGb * 1024; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static string HumanReadableSize(float? size) + internal static string HumanReadableSize(in float size) { if (size < UnitKb) { @@ -202,7 +202,7 @@ private void ProcessRequest(HttpListenerContext context) executionResult.Request = request; otherParty = request.RemoteAddress; - if (!matchedListeningHost.CanListen) + if (matchedListeningHost.Router is null) { baseResponse.StatusCode = 503; // Service Unavailable executionResult.Status = HttpServerExecutionStatus.ListeningHostNotReady; @@ -210,7 +210,7 @@ private void ProcessRequest(HttpListenerContext context) } else { - matchedListeningHost.Router!.BindServer(this); + matchedListeningHost.Router.BindServer(this); } #endregion // 27668 @@ -219,6 +219,7 @@ private void ProcessRequest(HttpListenerContext context) if (ServerConfiguration.IncludeRequestIdHeader) baseResponse.Headers.Set(flag.HeaderNameRequestId, request.RequestId.ToString()); + if (flag.SendSiskHeader) baseResponse.Headers.Set(HttpKnownHeaderNames.XPoweredBy, PoweredBy); @@ -492,6 +493,7 @@ response.Content is StreamContent || { ServerConfiguration.ErrorsLogsStream?.WriteException(executionResult.ServerException); } + if (canAccessLog) { var formatter = new LoggingFormatter( diff --git a/src/Http/ListeningHostRepository.cs b/src/Http/ListeningHostRepository.cs index 9dd89c7..3174400 100644 --- a/src/Http/ListeningHostRepository.cs +++ b/src/Http/ListeningHostRepository.cs @@ -115,7 +115,7 @@ IEnumerator IEnumerable.GetEnumerator() /// The Listening Host index public ListeningHost this[int index] { get => _hosts[index]; set => _hosts[index] = value; } - internal ListeningHost? GetRequestMatchingListeningHost(string incomingHost, int incomingPort) + internal ListeningHost? GetRequestMatchingListeningHost(string incomingHost, in int incomingPort) { lock (_hosts) { diff --git a/src/Http/Streams/HttpStreamPingPolicy.cs b/src/Http/Streams/HttpStreamPingPolicy.cs index 9c0a250..d6f5fdc1 100644 --- a/src/Http/Streams/HttpStreamPingPolicy.cs +++ b/src/Http/Streams/HttpStreamPingPolicy.cs @@ -26,7 +26,7 @@ public sealed class HttpStreamPingPolicy /// /// Gets or sets the sending interval for each ping message. /// - public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(1); + public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(5); internal HttpStreamPingPolicy(HttpRequestEventSource parent) { diff --git a/src/Internal/CookieParser.cs b/src/Internal/CookieParser.cs new file mode 100644 index 0000000..6ed108a --- /dev/null +++ b/src/Internal/CookieParser.cs @@ -0,0 +1,78 @@ +// The Sisk Framework source code +// Copyright (c) 2023 PROJECT PRINCIPIUM +// +// The code below is licensed under the MIT license as +// of the date of its publication, available at +// +// File name: CookieParser.cs +// Repository: https://github.com/sisk-http/core + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Sisk.Core.Internal; + +internal static class CookieParser +{ + public static string? RemoveValueQuotes(string? value) + { + if (value is null) + return null; + + const char QUOTE = '"'; + + if (value.StartsWith(QUOTE)) + { + value = value[1..]; + } + if (value.EndsWith(QUOTE)) + { + value = value[..^1]; + } + return value; + } + + public static NameValueCollection ParseCookieString(string? cookieHeader) + { + NameValueCollection cookies = new NameValueCollection(); + if (!string.IsNullOrWhiteSpace(cookieHeader)) + { + string[] cookiePairs = cookieHeader.Split(';'); + + for (int i = 0; i < cookiePairs.Length; i++) + { + string cookieExpression = cookiePairs[i]; + + if (string.IsNullOrWhiteSpace(cookieExpression)) + continue; + + int eqPos = cookieExpression.IndexOf('='); + if (eqPos < 0) + { + cookies[cookieExpression] = ""; + continue; + } + else + { + string cookieName = cookieExpression.Substring(0, eqPos).Trim(); + string cookieValue = cookieExpression.Substring(eqPos + 1).Trim(); + + if (string.IsNullOrWhiteSpace(cookieName)) + { + // provided an name/value pair, but no name + continue; + } + + cookies[cookieName] = WebUtility.UrlDecode(cookieValue); + } + } + } + + return cookies; + } +} diff --git a/src/Internal/HttpStringInternals.cs b/src/Internal/HttpStringInternals.cs index bc14e15..3e373cb 100644 --- a/src/Internal/HttpStringInternals.cs +++ b/src/Internal/HttpStringInternals.cs @@ -122,7 +122,7 @@ public static PathMatchResult IsPathMatch(ReadOnlySpan pathPattern, ReadOn return new PathMatchResult(true, query); } #else - public static bool PathRouteMatch(string routeA, string routeB, bool ignoreCase) + public static bool PathRouteMatch(in string routeA, in string routeB, bool ignoreCase) { if (routeA == Route.AnyPath || routeB == Route.AnyPath) { @@ -160,7 +160,7 @@ public static bool PathRouteMatch(string routeA, string routeB, bool ignoreCase) } [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public static PathMatchResult IsPathMatch(string pathPattern, string requestPath, bool ignoreCase) + public static PathMatchResult IsPathMatch(in string pathPattern, in string requestPath, bool ignoreCase) { if (pathPattern == Route.AnyPath) { @@ -201,7 +201,7 @@ public static PathMatchResult IsPathMatch(string pathPattern, string requestPath } #endif - public static bool IsDnsMatch(string wildcardPattern, string subject) + public static bool IsDnsMatch(in string wildcardPattern, string subject) { StringComparison comparer = StringComparison.OrdinalIgnoreCase; @@ -245,7 +245,7 @@ public static bool IsDnsMatch(string wildcardPattern, string subject) } } - private static bool isWildcardMatchRgx(string pattern, string subject, StringComparison comparer) + private static bool isWildcardMatchRgx(in string pattern, in string subject, StringComparison comparer) { string[] parts = pattern.Split('*'); if (parts.Length <= 1) diff --git a/src/Internal/LoggingConstants.cs b/src/Internal/LoggingConstants.cs index b9d14e6..566b191 100644 --- a/src/Internal/LoggingConstants.cs +++ b/src/Internal/LoggingConstants.cs @@ -26,8 +26,8 @@ internal class LoggingFormatter readonly int bResStatusCode; readonly string? bResStatusDescr; readonly string bReqMethod; - readonly float? incomingSize; - readonly float? outcomingSize; + readonly long incomingSize; + readonly long outcomingSize; readonly long execTime; public LoggingFormatter( @@ -47,8 +47,8 @@ public LoggingFormatter( this.reqHeaders = reqHeaders; this.bResStatusCode = bResStatusCode; this.bResStatusDescr = bResStatusDescr; - incomingSize = res.RequestSize; - outcomingSize = res.ResponseSize; + this.incomingSize = res.RequestSize; + this.outcomingSize = res.ResponseSize; this.execTime = execTime; this.bReqMethod = bReqMethod; } diff --git a/src/Internal/SR.cs b/src/Internal/SR.cs index 0a9542c..93bda01 100644 --- a/src/Internal/SR.cs +++ b/src/Internal/SR.cs @@ -12,6 +12,8 @@ static partial class SR public const string MultipartObject_ContentTypeMissing = "Content-Type header cannot be null when retriving a multipart form content"; public const string MultipartObject_BoundaryMissing = "No boundary was specified for this multipart form content."; public const string MultipartObject_EmptyFieldName = "Content-part object position {0} cannot have an empty field name."; + public const string MultipartFormReader_InvalidData = "Cannot read the specified multipart-form data request. At byte position {0}: {1}"; + public const string MultipartFormReader_Exception = "Caught an exception while trying to read the multipart form request: {0}"; public const string HttpContextBagRepository_UndefinedDynamicProperty = "The specified type {0} was not defined in this context bag repository."; diff --git a/src/Routing/Route.cs b/src/Routing/Route.cs index 2b28bd3..6ddbfda 100644 --- a/src/Routing/Route.cs +++ b/src/Routing/Route.cs @@ -15,7 +15,7 @@ namespace Sisk.Core.Routing /// /// Represents an HTTP route to be matched by an object. /// - public sealed class Route + public class Route { internal RouteAction? _callback { get; set; } internal bool isReturnTypeTask; diff --git a/src/Routing/Router__CoreInvoker.cs b/src/Routing/Router__CoreInvoker.cs index 997b757..98bf301 100644 --- a/src/Routing/Router__CoreInvoker.cs +++ b/src/Routing/Router__CoreInvoker.cs @@ -56,7 +56,7 @@ private Internal.HttpStringInternals.PathMatchResult TestRouteMatchUsingRegex(Ro } } - internal bool InvokeRequestHandlerGroup(RequestHandlerExecutionMode mode, IRequestHandler[] baseLists, IRequestHandler[]? bypassList, HttpRequest request, HttpContext context, out HttpResponse? result, out Exception? exception) + internal bool InvokeRequestHandlerGroup(in RequestHandlerExecutionMode mode, IRequestHandler[] baseLists, IRequestHandler[]? bypassList, HttpRequest request, HttpContext context, out HttpResponse? result, out Exception? exception) { ref IRequestHandler pointer = ref MemoryMarshal.GetArrayDataReference(baseLists); for (int i = 0; i < baseLists.Length; i++) @@ -124,7 +124,7 @@ internal RouterExecutionResult Execute(HttpContext context) ref Route rPointer = ref MemoryMarshal.GetReference(rspan); for (int i = 0; i < rspan.Length; i++) { - Route route = Unsafe.Add(ref rPointer, i); + ref Route route = ref Unsafe.Add(ref rPointer, i); // test path HttpStringInternals.PathMatchResult pathTest; diff --git a/src/Sisk.Core.csproj b/src/Sisk.Core.csproj index 4c10697..571d861 100644 --- a/src/Sisk.Core.csproj +++ b/src/Sisk.Core.csproj @@ -31,7 +31,7 @@ 1.0.0.0 1.0.0.0 - 1.0.0.0-rc3 + 1.0.0.0-rc4 LICENSE.txt False