< Summary

Information
Class: NanoRoute.Internals.QueryStringParser
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Private/QueryStringParser.cs
Line coverage
96%
Covered lines: 83
Uncovered lines: 3
Coverable lines: 86
Total lines: 171
Line coverage: 96.5%
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.Frozen;
 9using System.Collections.Generic;
 10using System.Diagnostics;
 11using System.Diagnostics.CodeAnalysis;
 12using System.Net;
 13using System.Net.Http;
 14using System.Threading.Tasks;
 15
 16namespace NanoRoute.Internals
 17{
 18    using Properties;
 19
 120    internal sealed class QueryStringParser(RequestContext context, FrozenDictionary<ReadOnlyMemory<char>, ParameterPars
 21    {
 22        #region Private
 23        // Extend only lists created by this parser; replace anything supplied by earlier middleware.
 24        private sealed class QueryValueList : List<object?>;
 25
 126        private static readonly ArrayPool<char> s_arrayPool = ArrayPool<char>.Create();
 27
 28        private char[]? _decodedBuffer;
 29
 30        // Track only query parameters seen during this parse so required checks and duplicate detection
 31        // will not be confused by values that were already present in context.Parameters.
 132        private readonly bool[] _visited = new bool[expectedParameters.Count];
 33
 34        // DelimitedSegment is a mutable struct: keep this field non-readonly so MoveNext() updates the _parameter
 35        // itself instead of a defensive copy.
 136        private DelimitedSegment _parameter = new(GetRawQuery(context.Request.RequestUri), '&');
 37
 38        private int _nextDecoded;
 39
 40        // Keep Parse() state-machine-free while all query value parsers complete synchronously.
 41        // If a parser really suspends, this helper resumes the current parameter and finishes the loop.
 42        private async ValueTask ParseAwaitedAsync(ParameterParser expectedParameter, ValueTask<ValueParseResult> parseRe
 43        {
 44            AcceptParameter(expectedParameter.Definition, await parseResult.ConfigureAwait(false));
 45            await Parse().ConfigureAwait(false);
 46        }
 47
 48        private void AcceptParameter(ParameterDefinition parameterDefinition, ValueParseResult parseResult)
 149        {
 150            if (!parseResult.Success)
 151                ThrowBadRequest(Resources.ERR_QUERY_INVALID_PARAMETER, parameterDefinition.ParameterName!);
 52
 153            if (parameterDefinition.ValueParser.IsList)
 154            {
 155                if (!context.Parameters.TryGetValue(parameterDefinition.ParameterName!, out object? val) || val is not Q
 156                    context.Parameters[parameterDefinition.ParameterName!] = lst = [];
 57
 158                lst.Add(parseResult.Parsed);
 159            }
 60            else
 161                context.Parameters[parameterDefinition.ParameterName!] = parseResult.Parsed;
 162        }
 63
 64        private ReadOnlyMemory<char> DecodeParameter(ReadOnlyMemory<char> source)
 165        {
 166            if (source.Span.IndexOfAny('%', '+') < 0)
 167                return source;
 68
 169            char[] decodedBuffer = _decodedBuffer ??= s_arrayPool.Rent(_parameter.Remaining.Length);
 70
 171            if (!UrlUtils.TryDecodeUrl(source.Span, decodedBuffer.AsSpan(_nextDecoded), UrlDecodeMode.Form, out int char
 172                ThrowBadRequest(Resources.ERR_DECODING_FAILED);
 73
 174            ReadOnlyMemory<char> result = decodedBuffer.AsMemory(_nextDecoded, charsWritten);
 175            _nextDecoded += charsWritten;
 76
 177            return result;
 178        }
 79
 80        private static ReadOnlyMemory<char> GetRawQuery(Uri uri)
 181        {
 182            ReadOnlyMemory<char> raw = uri.OriginalString.AsMemory();
 83
 184            int fragmentIndex = raw.Span.IndexOf('#');
 185            if (fragmentIndex >= 0)
 186                raw = raw.Slice(0, fragmentIndex);
 87
 188            int queryIndex = raw.Span.IndexOf('?');
 189            return queryIndex >= 0
 190                ? raw.Slice(queryIndex + 1)
 191                : default;
 192        }
 93
 94        [DoesNotReturn]
 195        private static void ThrowBadRequest(string? error, params object[] paramz) => HttpRequestException.Throw
 196        (
 197            HttpStatusCode.BadRequest,
 198            Resources.ERR_BAD_REQUEST,
 199            !string.IsNullOrEmpty(error) ? [string.Format(Resources.Culture, error, paramz)] : []
 1100        );
 101        #endregion
 102
 103        public void Dispose()
 1104        {
 1105            if (_decodedBuffer is not null)
 1106                s_arrayPool.Return(_decodedBuffer, clearArray: false);
 1107        }
 108
 109        public ValueTask Parse()
 1110        {
 1111            while (_parameter.MoveNext())
 1112            {
 1113                int separatorIndex = _parameter.Current.Span.IndexOf('=');
 1114                if (separatorIndex <= 0)
 1115                    ThrowBadRequest(null);
 116
 1117                ReadOnlyMemory<char> parameterName = DecodeParameter(_parameter.Current.Slice(0, separatorIndex));
 118
 1119                if (!expectedParameters.TryGetValue(parameterName, out ParameterParser? expectedParameter))
 1120                {
 1121                    switch (config.UnexpectedParameterBehavior)
 122                    {
 123                        case UnexpectedParameterBehavior.Ignore:
 1124                            continue;
 125
 126                        case UnexpectedParameterBehavior.Reject:
 1127                            ThrowBadRequest(Resources.ERR_QUERY_UNEXPECTED_PARAMETER, parameterName);
 128                            break;
 129
 130                        default:
 0131                            Debug.Fail($"Unknown {nameof(UnexpectedParameterBehavior)} value: {config.UnexpectedParamete
 0132                            break;
 133                    }
 0134                }
 135
 1136                ParameterDefinition parameterDefinition = expectedParameter!.Definition;
 137
 1138                if (_visited[parameterDefinition.Index] && !parameterDefinition.ValueParser.IsList)
 1139                    ThrowBadRequest(Resources.ERR_QUERY_DUPLICATE_PARAMETER, parameterDefinition.ParameterName!);
 140
 1141                _visited[parameterDefinition.Index] = true;
 142
 1143                ValueTask<ValueParseResult> parseResult = expectedParameter.Parse
 1144                (
 1145                    new ValueParserContext
 1146                    {
 1147                        Segment = DecodeParameter(_parameter.Current.Slice(separatorIndex + 1)),
 1148                        Services = context.Services,
 1149                        Arguments = expectedParameter.Arguments,
 1150                        Cancellation = context.Cancellation
 1151                    }
 1152                );
 153
 1154                if (!parseResult.IsCompletedSuccessfully)
 1155                    return ParseAwaitedAsync(expectedParameter, parseResult);
 156
 1157                AcceptParameter(parameterDefinition, parseResult.Result);
 1158            }
 159
 1160            foreach (ParameterParser expectedParameter in expectedParameters.Values)
 1161            {
 1162                ParameterDefinition parameterDefinition = expectedParameter.Definition;
 163
 1164                if (!parameterDefinition.IsOptional && !_visited[parameterDefinition.Index])
 1165                    ThrowBadRequest(Resources.ERR_QUERY_MISSING_PARAMETER, parameterDefinition.ParameterName!);
 1166            }
 167
 1168            return default;
 1169        }
 170    }
 171}