< Summary

Information
Class: NanoRoute.Internals.QueryStringParser
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Private/QueryStringParser.cs
Line coverage
100%
Covered lines: 75
Uncovered lines: 0
Coverable lines: 75
Total lines: 148
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Private/QueryStringParser.cs

#LineLine coverage
 1/********************************************************************************
 2* QueryStringParser.cs                                                          *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Buffers;
 8using System.Collections.Generic;
 9using System.Diagnostics.CodeAnalysis;
 10using System.Net;
 11using System.Net.Http;
 12using System.Threading.Tasks;
 13
 14namespace NanoRoute.Internals
 15{
 16    using Properties;
 17
 118    internal sealed class QueryStringParser(RequestContext context, IReadOnlyDictionary<ReadOnlyMemory<char>, ParameterP
 19    {
 20        #region Private
 121        private static readonly ArrayPool<char> s_arrayPool = ArrayPool<char>.Create();
 22
 123        private readonly ReadOnlyMemory<char> _query = GetRawQuery(context.Request.RequestUri);
 24
 25        private char[]? _decodedBuffer;
 26
 27        // Track only query parameters seen during this parse so required checks and duplicate detection
 28        // will not be confused by values that were already present in context.Parameters.
 129        private readonly bool[] _visited = new bool[expectedParameters.Count];
 30
 31        // DelimitedSegment is a mutable struct: keep this field non-readonly so MoveNext() updates the _parameter
 32        // itself instead of a defensive copy.
 133        private DelimitedSegment _parameter = new(GetRawQuery(context.Request.RequestUri), '&');
 34
 35        private int _nextDecoded;
 36
 37        // Keep Parse() state-machine-free while all query value parsers complete synchronously.
 38        // If a parser really suspends, this helper resumes the current parameter and finishes the loop.
 39        private async ValueTask ParseAwaitedAsync(ParameterParser expectedParameter, ValueTask<ValueParseResult> parseRe
 40        {
 41            AcceptParameter(expectedParameter.Definition, await parseResult);
 42            await Parse();
 43        }
 44
 45        private void AcceptParameter(ParameterDefinition parameterDefinition, ValueParseResult parseResult)
 146        {
 147            if (!parseResult.Success)
 148                ThrowBadRequest(Resources.ERR_QUERY_INVALID_PARAMETER, parameterDefinition.ParameterName!);
 49
 150            context.Parameters[parameterDefinition.ParameterName!] = parseResult.Parsed;
 151        }
 52
 53        private ReadOnlyMemory<char> DecodeParameter(ReadOnlyMemory<char> source)
 154        {
 155            if (source.Span.IndexOfAny('%', '+') < 0)
 156                return source;
 57
 158            char[] decodedBuffer = _decodedBuffer ??= s_arrayPool.Rent(_query.Length);
 59
 160            if (!UrlUtils.TryDecodeUrl(source.Span, decodedBuffer.AsSpan(_nextDecoded), UrlDecodeMode.Form, out int char
 161                ThrowBadRequest(Resources.ERR_DECODING_FAILED);
 62
 163            ReadOnlyMemory<char> result = decodedBuffer.AsMemory(_nextDecoded, charsWritten);
 164            _nextDecoded += charsWritten;
 65
 166            return result;
 167        }
 68
 69        private static ReadOnlyMemory<char> GetRawQuery(Uri uri)
 170        {
 171            ReadOnlyMemory<char> raw = uri.OriginalString.AsMemory();
 72
 173            int fragmentIndex = raw.Span.IndexOf('#');
 174            if (fragmentIndex >= 0)
 175                raw = raw.Slice(0, fragmentIndex);
 76
 177            int queryIndex = raw.Span.IndexOf('?');
 178            return queryIndex >= 0
 179                ? raw.Slice(queryIndex + 1)
 180                : default;
 181        }
 82
 83        [DoesNotReturn]
 184        private static void ThrowBadRequest(string? error, params object[] paramz) => HttpRequestException.Throw
 185        (
 186            HttpStatusCode.BadRequest,
 187            Resources.ERR_BAD_REQUEST,
 188            !string.IsNullOrEmpty(error) ? [string.Format(Resources.Culture, error, paramz)] : []
 189        );
 90        #endregion
 91
 92        public void Dispose()
 193        {
 194            if (_decodedBuffer is not null)
 195                s_arrayPool.Return(_decodedBuffer, clearArray: false);
 196        }
 97
 98        public ValueTask Parse()
 199        {
 1100            while (_parameter.MoveNext())
 1101            {
 1102                int separatorIndex = _parameter.Current.Span.IndexOf('=');
 1103                if (separatorIndex <= 0)
 1104                    ThrowBadRequest(null);
 105
 1106                ReadOnlyMemory<char> parameterName = DecodeParameter(_parameter.Current.Slice(0, separatorIndex));
 107
 1108                if (!expectedParameters.TryGetValue(parameterName, out ParameterParser? expectedParameter))
 1109                    continue;
 110
 1111                ParameterDefinition parameterDefinition = expectedParameter.Definition;
 112
 1113                if (_visited[parameterDefinition.Index])
 1114                    ThrowBadRequest(Resources.ERR_QUERY_DUPLICATE_PARAMETER, parameterDefinition.ParameterName!);
 115
 1116                _visited[parameterDefinition.Index] = true;
 117
 1118                ValueTask<ValueParseResult> parseResult = expectedParameter.Parse
 1119                (
 1120                    new ValueParserContext
 1121                    {
 1122                        Segment = DecodeParameter(_parameter.Current.Slice(separatorIndex + 1)),
 1123                        Services = context.Services,
 1124                        Arguments = expectedParameter.Arguments,
 1125                        Cancellation = context.Cancellation
 1126                    }
 1127                );
 128
 1129                if (!parseResult.IsCompletedSuccessfully)
 1130                    return ParseAwaitedAsync(expectedParameter, parseResult);
 131
 1132                AcceptParameter(parameterDefinition, parseResult.Result);
 1133            }
 134
 135            // FrozenDictionary uses ImmutableArray to access Values so accessing this property is a relative fast opera
 136            // https://learn.microsoft.com/en-us/dotnet/api/system.collections.frozen.frozendictionary-2.values?view=net
 1137            foreach (ParameterParser expectedParameter in expectedParameters.Values)
 1138            {
 1139                ParameterDefinition parameterDefinition = expectedParameter.Definition;
 140
 1141                if (!parameterDefinition.IsOptional && !_visited[parameterDefinition.Index])
 1142                    ThrowBadRequest(Resources.ERR_QUERY_MISSING_PARAMETER, parameterDefinition.ParameterName!);
 1143            }
 144
 1145            return default;
 1146        }
 147    }
 148}