< Summary

Information
Class: NanoRoute.Internals.RouteMatchCursor
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Private/RouteMatchCursor.cs
Line coverage
98%
Covered lines: 186
Uncovered lines: 2
Coverable lines: 188
Total lines: 391
Line coverage: 98.9%
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/RouteMatchCursor.cs

#LineLine coverage
 1/********************************************************************************
 2* RouteMatchCursor.cs                                                           *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Buffers;
 8using System.Collections.Generic;
 9using System.Diagnostics;
 10using System.Net;
 11using System.Net.Http;
 12using System.Runtime.CompilerServices;
 13using System.Threading;
 14using System.Threading.Tasks;
 15
 16namespace NanoRoute.Internals
 17{
 18    using Properties;
 19
 20    internal sealed class RouteMatchCursor : IDisposable
 21    {
 22        #region Private
 23        private enum BranchKind : byte
 24        {
 25            Literal,
 26            Parsed
 27        }
 28
 29        private enum MatchPhase : byte
 30        {
 31            /// <summary>
 32            /// Emit all handlers that belong to the current node before descending into child nodes.
 33            /// </summary>
 34            EmitHandlers,
 35
 36            /// <summary>
 37            /// Explore child branches according to the configured precedence.
 38            /// </summary>
 39            Branch,
 40
 41            /// <summary>
 42            /// Terminating step
 43            /// </summary>
 44            Done
 45        }
 46
 47        private readonly record struct BranchOrder(BranchKind First, BranchKind Second)
 48        {
 249            public static BranchOrder From(MatchingPrecedence matchingPrecedence) => matchingPrecedence switch
 250            {
 251                MatchingPrecedence.LiteralFirst => new BranchOrder(BranchKind.Literal, BranchKind.Parsed),
 252                MatchingPrecedence.ParameterizedFirst => new BranchOrder(BranchKind.Parsed, BranchKind.Literal),
 253                _ => throw new ArgumentOutOfRangeException(nameof(matchingPrecedence))  // dead code (valid values enfor
 254            };
 55        }
 56
 257        private static readonly ArrayPool<char> s_arrayPool = ArrayPool<char>.Create();
 58
 259        private static readonly ValueTask<bool>
 260            s_true = new(true),
 261            s_false = new(false);
 62
 63        private readonly BranchOrder _branchOrder;
 64
 65        private readonly RouteNode _root;
 66
 67        private char[]? _decodedSegmentBuffer;
 68
 69        // Keep DelimitedSegment instead of Uri.Segments: UrlSegmentBenchmarks shows it avoids eager segment
 70        // array/string allocation and preserves this cursor's lazy traversal model.
 71        private DelimitedSegment _segment;
 72
 73        private MatchPhase _phase;
 74
 75        private int
 76            _handlerIndex,
 77            _nextDecodedSegment;
 78
 79        private RouteNode _node;
 80
 81        #region Async helpers
 82        // Keep MoveNextAsync() state-machine-free while branch matching completes synchronously
 83        private async ValueTask<bool> MoveNextAwaitedAsync(ValueTask<bool> branchMatched)
 84        {
 85            if (!await branchMatched.ConfigureAwait(false))
 86            {
 87                _phase = MatchPhase.Done;
 88                return false;
 89            }
 90
 91            _phase = GetPhaseForCurrentNode();
 92            return await MoveNextAsync().ConfigureAwait(false);
 93        }
 94
 95        private async ValueTask<bool> TryBranchPairAwaitedAsync(ValueTask<bool> firstBranchMatched, ReadOnlyMemory<char>
 96            await firstBranchMatched.ConfigureAwait(false) ||
 97            await TryBranchAsync(_branchOrder.Second, decodedSegment).ConfigureAwait(false);
 98
 99        private async ValueTask<bool> TryParsedBranchAwaitedAsync(KeyValuePair<ParameterParser, RouteNode> parsedBranch,
 100            TryParsedBranchThenAdvance(parsedBranch, await parseResult.ConfigureAwait(false)) ||
 101            await TryParsedBranchAsync(decodedSegment, branchIndex + 1).ConfigureAwait(false);
 102
 103        private async ValueTask<bool> TryAcceptParsedBranchAwaitedAsync(KeyValuePair<ParameterParser, RouteNode> parsedB
 104            TryParsedBranchThenAdvance(parsedBranch, await parseResult.ConfigureAwait(false)) &&
 105            await TrySingleBranchesAsync();
 106        #endregion
 107
 108        private ReadOnlyMemory<char> DecodeIfNeeded(ReadOnlyMemory<char> segment)
 2109        {
 110            // This check is fast since span operations are vectorized
 2111            if (segment.Span.IndexOf('%') < 0)
 2112                return segment;
 113
 1114            char[] decodedSegmentBuffer = _decodedSegmentBuffer ??= s_arrayPool.Rent(_segment.Remaining.Length);
 115
 1116            if (!UrlUtils.TryDecodeUrl(segment.Span, decodedSegmentBuffer.AsSpan(_nextDecodedSegment), UrlDecodeMode.Pat
 1117                HttpRequestException.Throw(HttpStatusCode.BadRequest, Resources.ERR_BAD_REQUEST, Resources.ERR_DECODING_
 118
 1119            ReadOnlyMemory<char> decodedSegment = decodedSegmentBuffer.AsMemory(_nextDecodedSegment, charsWritten);
 1120            _nextDecodedSegment += charsWritten;
 1121            return decodedSegment;
 2122        }
 123
 124        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 125        private void AdvanceToNextSegment(RouteNode nextNode)
 2126        {
 2127            _node = nextNode;
 2128            _handlerIndex = 0;
 129
 2130            _segment.MoveNext();
 2131        }
 132
 133        internal bool TryEmitHandler()
 2134        {
 2135            while (_handlerIndex < _node.Handlers.Count)
 2136            {
 2137                KeyValuePair<HttpVerb, HandlerRegistration> handler = _node.Handlers[_handlerIndex++];
 138
 2139                if (handler.Key != Verb)
 1140                    continue;
 141
 2142                HandlerRegistration candidate = handler.Value;
 143
 2144                if (_segment.HasValue && !candidate.IsPrefix)
 1145                    continue;
 146
 2147                HandlerRegistration = candidate;
 2148                return true;
 149            }
 150
 1151            return false;
 2152        }
 153
 154        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 2155        private MatchPhase GetPhaseForCurrentNode() => _node.Handlers.Count > 0
 2156            ? MatchPhase.EmitHandlers
 2157            : MatchPhase.Branch;
 158
 159        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1160        private ValueTask<bool> TryBranchAsync(BranchKind branchKind, ReadOnlyMemory<char> decodedSegment) => branchKind
 1161        {
 1162            BranchKind.Literal => new ValueTask<bool>(TryLiteralBranchThenAdvance(decodedSegment)),
 1163            BranchKind.Parsed => TryParsedBranchAsync(decodedSegment, 0),
 1164            _ => throw new ArgumentOutOfRangeException(nameof(branchKind))
 1165        };
 166
 167        internal ValueTask<bool> TrySingleBranchesAsync()
 2168        {
 169            // clean up recent state
 2170            _handlerIndex = 0;
 171
 172            // PERF: do not remove these local variables
 2173            DelimitedSegment segment = _segment;
 2174            RouteNode node = _node;
 175
 2176            for (; segment.HasValue && node.SingleBranch is { } branch; segment.MoveNext())
 2177            {
 2178                ReadOnlyMemory<char> segmentChars = DecodeIfNeeded(segment.Current);
 179
 2180                switch (branch)
 181                {
 182                    case KeyValuePair<ReadOnlyMemory<char>, RouteNode> literalBranch:
 2183                        if (!ReadOnlyMemoryCharComparer.Instance.Equals(literalBranch.Key, segmentChars))
 1184                            return s_false;
 185
 2186                        node = literalBranch.Value;
 2187                        break;
 188
 189                    case KeyValuePair<ParameterParser, RouteNode> parsedBranch:
 1190                        ValueTask<ValueParseResult> parseResultTask = ParseSegment(segmentChars, parsedBranch.Key);
 191
 1192                        if (!parseResultTask.IsCompletedSuccessfully)
 1193                        {
 1194                            _segment = segment;
 1195                            return TryAcceptParsedBranchAwaitedAsync(parsedBranch, parseResultTask);
 196                        }
 197
 1198                        if (!TryParsedBranch(parsedBranch.Key.Definition, parseResultTask.Result))
 1199                            return s_false;
 200
 1201                        node = parsedBranch.Value;
 1202                        break;
 203
 204                    default:
 0205                        Debug.Fail($"Unknown single branch type: {branch.GetType().Name}");
 0206                        return s_false;
 207                }
 2208            }
 209
 2210            _segment = segment;
 2211            _node = node;
 212
 2213            return s_true;
 2214        }
 215
 216        private ValueTask<bool> TryBranchPairAsync()
 1217        {
 218            // Decode only when the matcher is about to inspect the segment. Prefix handlers can still run
 219            // without paying this cost and they can catch invalid escape errors, too.
 1220            ReadOnlyMemory<char> segment = DecodeIfNeeded(_segment.Current);
 221
 1222            ValueTask<bool> firstBranchMatched = TryBranchAsync(_branchOrder.First, segment);
 223
 1224            if (!firstBranchMatched.IsCompletedSuccessfully)
 1225                return TryBranchPairAwaitedAsync(firstBranchMatched, segment);
 226
 1227            if (firstBranchMatched.Result)
 1228                return s_true;
 229
 1230            return TryBranchAsync(_branchOrder.Second, segment);
 1231        }
 232
 233        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1234        private ValueTask<ValueParseResult> ParseSegment(ReadOnlyMemory<char> decodedSegment, ParameterParser parser) =>
 1235        (
 1236            new ValueParserContext
 1237            {
 1238                Segment = decodedSegment,
 1239                Services = Services,
 1240                Arguments = parser.Arguments,
 1241                Cancellation = Cancellation
 1242            }
 1243        );
 244
 245        private ValueTask<bool> TryParsedBranchAsync(ReadOnlyMemory<char> decodedSegment, int startIndex)
 1246        {
 1247            IList<KeyValuePair<ParameterParser, RouteNode>> parsedBranches = _node.ParsedBranches;
 248
 1249            for (int i = startIndex; i < parsedBranches.Count; i++)
 1250            {
 1251                KeyValuePair<ParameterParser, RouteNode> parsedBranch = parsedBranches[i];
 252
 1253                ValueTask<ValueParseResult> parseResult = ParseSegment(decodedSegment, parsedBranch.Key);
 254
 1255                if (!parseResult.IsCompletedSuccessfully)
 1256                    return TryParsedBranchAwaitedAsync(parsedBranch, parseResult, decodedSegment, i);
 257
 1258                if (TryParsedBranchThenAdvance(parsedBranch, parseResult.Result))
 1259                    return s_true;
 1260            }
 261
 1262            return s_false;
 1263        }
 264
 265        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 266        private bool TryParsedBranch(ParameterDefinition parameter, ValueParseResult parseResult)
 1267        {
 1268            if (!parseResult.Success)
 1269                return false;
 270
 1271            if (parameter.ParameterName is { Length: > 0 } parameterName)
 272                // This will overwrite any existing parameter on the given key
 1273                Parameters[parameterName] = parseResult.Parsed;
 274
 1275            return true;
 1276        }
 277
 278        private bool TryLiteralBranchThenAdvance(ReadOnlyMemory<char> decodedSegment)
 1279        {
 1280            if (!_node.LiteralBranches.TryGetValue(decodedSegment, out RouteNode literalBranch))
 1281                return false;
 282
 1283            AdvanceToNextSegment(literalBranch);
 1284            return true;
 1285        }
 286
 287        private bool TryParsedBranchThenAdvance(KeyValuePair<ParameterParser, RouteNode> parsedBranch, ValueParseResult 
 1288        {
 1289            if (!TryParsedBranch(parsedBranch.Key.Definition, parseResult))
 1290                return false;
 291
 1292            AdvanceToNextSegment(parsedBranch.Value);
 1293            return true;
 1294        }
 295        #endregion
 296
 2297        public RouteMatchCursor(RouteNode node, HttpVerb verb, Uri uri, IServiceProvider services, IDictionary<string, o
 2298        {
 2299            _segment = new DelimitedSegment
 2300            (
 2301                // Escaped path, not percent decoded -> "/path%2Fto%2Fsomewhere/" will be treated as a single segment
 2302                uri.AbsolutePath.AsMemory(),
 2303                '/'
 2304            );
 2305            _branchOrder = BranchOrder.From(matchingPrecedence);
 2306            _root = _node = node;
 307
 2308            Cancellation = cancellation;
 2309            Parameters = parameters;
 2310            Services = services;
 2311            Verb = verb;
 312
 2313            AdvanceToNextSegment(_root);
 2314            _phase = GetPhaseForCurrentNode();
 2315        }
 316
 2317        public HandlerRegistration HandlerRegistration { get; private set; } = null!;
 318
 2319        public ReadOnlyMemory<char> RemainingPath => _segment.Remaining;
 320
 321        public CancellationToken Cancellation { get; }
 322
 323        public IServiceProvider Services { get; }
 324
 325        public IDictionary<string, object?> Parameters { get; }
 326
 327        public HttpVerb Verb { get; }
 328
 2329        public bool Completed => _phase is MatchPhase.Done;
 330
 331        public void Dispose()
 2332        {
 2333            if (_decodedSegmentBuffer is not null)
 1334            {
 1335                s_arrayPool.Return(_decodedSegmentBuffer, clearArray: false);
 1336                _decodedSegmentBuffer = null;
 1337            }
 2338        }
 339
 340        public void Reset()
 1341        {
 1342            HandlerRegistration = null!;
 1343            _nextDecodedSegment = 0;
 344
 1345            _segment.Reset();
 1346            AdvanceToNextSegment(_root);
 1347            _phase = GetPhaseForCurrentNode();
 1348        }
 349
 350        public ValueTask<bool> MoveNextAsync()
 2351        {
 2352            while (!Completed)
 2353            {
 2354                if (_phase is MatchPhase.EmitHandlers)
 2355                {
 2356                    Cancellation.ThrowIfCancellationRequested();
 357
 2358                    if (TryEmitHandler())
 2359                        return s_true;
 360
 361                    // No handler terminated the pipeline, go to the branch matching phase.
 1362                    _phase = MatchPhase.Branch;
 1363                }
 364
 2365                Debug.Assert(_phase is MatchPhase.Branch, $"Unknown phase: {_phase}");
 366
 2367                if (_segment.HasValue)
 2368                {
 2369                    ValueTask<bool> branchMatched = _node.SingleBranch is not null
 2370                        // fast path
 2371                        ? TrySingleBranchesAsync()
 2372                        : TryBranchPairAsync();
 373
 2374                    if (!branchMatched.IsCompletedSuccessfully)
 1375                        return MoveNextAwaitedAsync(branchMatched);
 376
 2377                    if (branchMatched.Result)
 2378                    {
 2379                        _phase = GetPhaseForCurrentNode();
 2380                        continue;
 381                    }
 1382                }
 383
 1384                _phase = MatchPhase.Done;
 1385                break;
 386            }
 387
 1388            return s_false;
 2389        }
 390    }
 391}