| | | 1 | | /******************************************************************************** |
| | | 2 | | * RouteMatchCursor.cs * |
| | | 3 | | * * |
| | | 4 | | * Author: Denes Solti * |
| | | 5 | | ********************************************************************************/ |
| | | 6 | | using System; |
| | | 7 | | using System.Buffers; |
| | | 8 | | using System.Collections.Generic; |
| | | 9 | | using System.Diagnostics; |
| | | 10 | | using System.Net; |
| | | 11 | | using System.Net.Http; |
| | | 12 | | using System.Runtime.CompilerServices; |
| | | 13 | | using System.Threading; |
| | | 14 | | using System.Threading.Tasks; |
| | | 15 | | |
| | | 16 | | namespace 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 | | { |
| | 2 | 49 | | public static BranchOrder From(MatchingPrecedence matchingPrecedence) => matchingPrecedence switch |
| | 2 | 50 | | { |
| | 2 | 51 | | MatchingPrecedence.LiteralFirst => new BranchOrder(BranchKind.Literal, BranchKind.Parsed), |
| | 2 | 52 | | MatchingPrecedence.ParameterizedFirst => new BranchOrder(BranchKind.Parsed, BranchKind.Literal), |
| | 2 | 53 | | _ => throw new ArgumentOutOfRangeException(nameof(matchingPrecedence)) // dead code (valid values enfor |
| | 2 | 54 | | }; |
| | | 55 | | } |
| | | 56 | | |
| | 2 | 57 | | private static readonly ArrayPool<char> s_arrayPool = ArrayPool<char>.Create(); |
| | | 58 | | |
| | 2 | 59 | | private static readonly ValueTask<bool> |
| | 2 | 60 | | s_true = new(true), |
| | 2 | 61 | | 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) |
| | 2 | 109 | | { |
| | | 110 | | // This check is fast since span operations are vectorized |
| | 2 | 111 | | if (segment.Span.IndexOf('%') < 0) |
| | 2 | 112 | | return segment; |
| | | 113 | | |
| | 1 | 114 | | char[] decodedSegmentBuffer = _decodedSegmentBuffer ??= s_arrayPool.Rent(_segment.Remaining.Length); |
| | | 115 | | |
| | 1 | 116 | | if (!UrlUtils.TryDecodeUrl(segment.Span, decodedSegmentBuffer.AsSpan(_nextDecodedSegment), UrlDecodeMode.Pat |
| | 1 | 117 | | HttpRequestException.Throw(HttpStatusCode.BadRequest, Resources.ERR_BAD_REQUEST, Resources.ERR_DECODING_ |
| | | 118 | | |
| | 1 | 119 | | ReadOnlyMemory<char> decodedSegment = decodedSegmentBuffer.AsMemory(_nextDecodedSegment, charsWritten); |
| | 1 | 120 | | _nextDecodedSegment += charsWritten; |
| | 1 | 121 | | return decodedSegment; |
| | 2 | 122 | | } |
| | | 123 | | |
| | | 124 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 125 | | private void AdvanceToNextSegment(RouteNode nextNode) |
| | 2 | 126 | | { |
| | 2 | 127 | | _node = nextNode; |
| | 2 | 128 | | _handlerIndex = 0; |
| | | 129 | | |
| | 2 | 130 | | _segment.MoveNext(); |
| | 2 | 131 | | } |
| | | 132 | | |
| | | 133 | | internal bool TryEmitHandler() |
| | 2 | 134 | | { |
| | 2 | 135 | | while (_handlerIndex < _node.Handlers.Count) |
| | 2 | 136 | | { |
| | 2 | 137 | | KeyValuePair<HttpVerb, HandlerRegistration> handler = _node.Handlers[_handlerIndex++]; |
| | | 138 | | |
| | 2 | 139 | | if (handler.Key != Verb) |
| | 1 | 140 | | continue; |
| | | 141 | | |
| | 2 | 142 | | HandlerRegistration candidate = handler.Value; |
| | | 143 | | |
| | 2 | 144 | | if (_segment.HasValue && !candidate.IsPrefix) |
| | 1 | 145 | | continue; |
| | | 146 | | |
| | 2 | 147 | | HandlerRegistration = candidate; |
| | 2 | 148 | | return true; |
| | | 149 | | } |
| | | 150 | | |
| | 1 | 151 | | return false; |
| | 2 | 152 | | } |
| | | 153 | | |
| | | 154 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 2 | 155 | | private MatchPhase GetPhaseForCurrentNode() => _node.Handlers.Count > 0 |
| | 2 | 156 | | ? MatchPhase.EmitHandlers |
| | 2 | 157 | | : MatchPhase.Branch; |
| | | 158 | | |
| | | 159 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 1 | 160 | | private ValueTask<bool> TryBranchAsync(BranchKind branchKind, ReadOnlyMemory<char> decodedSegment) => branchKind |
| | 1 | 161 | | { |
| | 1 | 162 | | BranchKind.Literal => new ValueTask<bool>(TryLiteralBranchThenAdvance(decodedSegment)), |
| | 1 | 163 | | BranchKind.Parsed => TryParsedBranchAsync(decodedSegment, 0), |
| | 1 | 164 | | _ => throw new ArgumentOutOfRangeException(nameof(branchKind)) |
| | 1 | 165 | | }; |
| | | 166 | | |
| | | 167 | | internal ValueTask<bool> TrySingleBranchesAsync() |
| | 2 | 168 | | { |
| | | 169 | | // clean up recent state |
| | 2 | 170 | | _handlerIndex = 0; |
| | | 171 | | |
| | | 172 | | // PERF: do not remove these local variables |
| | 2 | 173 | | DelimitedSegment segment = _segment; |
| | 2 | 174 | | RouteNode node = _node; |
| | | 175 | | |
| | 2 | 176 | | for (; segment.HasValue && node.SingleBranch is { } branch; segment.MoveNext()) |
| | 2 | 177 | | { |
| | 2 | 178 | | ReadOnlyMemory<char> segmentChars = DecodeIfNeeded(segment.Current); |
| | | 179 | | |
| | 2 | 180 | | switch (branch) |
| | | 181 | | { |
| | | 182 | | case KeyValuePair<ReadOnlyMemory<char>, RouteNode> literalBranch: |
| | 2 | 183 | | if (!ReadOnlyMemoryCharComparer.Instance.Equals(literalBranch.Key, segmentChars)) |
| | 1 | 184 | | return s_false; |
| | | 185 | | |
| | 2 | 186 | | node = literalBranch.Value; |
| | 2 | 187 | | break; |
| | | 188 | | |
| | | 189 | | case KeyValuePair<ParameterParser, RouteNode> parsedBranch: |
| | 1 | 190 | | ValueTask<ValueParseResult> parseResultTask = ParseSegment(segmentChars, parsedBranch.Key); |
| | | 191 | | |
| | 1 | 192 | | if (!parseResultTask.IsCompletedSuccessfully) |
| | 1 | 193 | | { |
| | 1 | 194 | | _segment = segment; |
| | 1 | 195 | | return TryAcceptParsedBranchAwaitedAsync(parsedBranch, parseResultTask); |
| | | 196 | | } |
| | | 197 | | |
| | 1 | 198 | | if (!TryParsedBranch(parsedBranch.Key.Definition, parseResultTask.Result)) |
| | 1 | 199 | | return s_false; |
| | | 200 | | |
| | 1 | 201 | | node = parsedBranch.Value; |
| | 1 | 202 | | break; |
| | | 203 | | |
| | | 204 | | default: |
| | 0 | 205 | | Debug.Fail($"Unknown single branch type: {branch.GetType().Name}"); |
| | 0 | 206 | | return s_false; |
| | | 207 | | } |
| | 2 | 208 | | } |
| | | 209 | | |
| | 2 | 210 | | _segment = segment; |
| | 2 | 211 | | _node = node; |
| | | 212 | | |
| | 2 | 213 | | return s_true; |
| | 2 | 214 | | } |
| | | 215 | | |
| | | 216 | | private ValueTask<bool> TryBranchPairAsync() |
| | 1 | 217 | | { |
| | | 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. |
| | 1 | 220 | | ReadOnlyMemory<char> segment = DecodeIfNeeded(_segment.Current); |
| | | 221 | | |
| | 1 | 222 | | ValueTask<bool> firstBranchMatched = TryBranchAsync(_branchOrder.First, segment); |
| | | 223 | | |
| | 1 | 224 | | if (!firstBranchMatched.IsCompletedSuccessfully) |
| | 1 | 225 | | return TryBranchPairAwaitedAsync(firstBranchMatched, segment); |
| | | 226 | | |
| | 1 | 227 | | if (firstBranchMatched.Result) |
| | 1 | 228 | | return s_true; |
| | | 229 | | |
| | 1 | 230 | | return TryBranchAsync(_branchOrder.Second, segment); |
| | 1 | 231 | | } |
| | | 232 | | |
| | | 233 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 1 | 234 | | private ValueTask<ValueParseResult> ParseSegment(ReadOnlyMemory<char> decodedSegment, ParameterParser parser) => |
| | 1 | 235 | | ( |
| | 1 | 236 | | new ValueParserContext |
| | 1 | 237 | | { |
| | 1 | 238 | | Segment = decodedSegment, |
| | 1 | 239 | | Services = Services, |
| | 1 | 240 | | Arguments = parser.Arguments, |
| | 1 | 241 | | Cancellation = Cancellation |
| | 1 | 242 | | } |
| | 1 | 243 | | ); |
| | | 244 | | |
| | | 245 | | private ValueTask<bool> TryParsedBranchAsync(ReadOnlyMemory<char> decodedSegment, int startIndex) |
| | 1 | 246 | | { |
| | 1 | 247 | | IList<KeyValuePair<ParameterParser, RouteNode>> parsedBranches = _node.ParsedBranches; |
| | | 248 | | |
| | 1 | 249 | | for (int i = startIndex; i < parsedBranches.Count; i++) |
| | 1 | 250 | | { |
| | 1 | 251 | | KeyValuePair<ParameterParser, RouteNode> parsedBranch = parsedBranches[i]; |
| | | 252 | | |
| | 1 | 253 | | ValueTask<ValueParseResult> parseResult = ParseSegment(decodedSegment, parsedBranch.Key); |
| | | 254 | | |
| | 1 | 255 | | if (!parseResult.IsCompletedSuccessfully) |
| | 1 | 256 | | return TryParsedBranchAwaitedAsync(parsedBranch, parseResult, decodedSegment, i); |
| | | 257 | | |
| | 1 | 258 | | if (TryParsedBranchThenAdvance(parsedBranch, parseResult.Result)) |
| | 1 | 259 | | return s_true; |
| | 1 | 260 | | } |
| | | 261 | | |
| | 1 | 262 | | return s_false; |
| | 1 | 263 | | } |
| | | 264 | | |
| | | 265 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 266 | | private bool TryParsedBranch(ParameterDefinition parameter, ValueParseResult parseResult) |
| | 1 | 267 | | { |
| | 1 | 268 | | if (!parseResult.Success) |
| | 1 | 269 | | return false; |
| | | 270 | | |
| | 1 | 271 | | if (parameter.ParameterName is { Length: > 0 } parameterName) |
| | | 272 | | // This will overwrite any existing parameter on the given key |
| | 1 | 273 | | Parameters[parameterName] = parseResult.Parsed; |
| | | 274 | | |
| | 1 | 275 | | return true; |
| | 1 | 276 | | } |
| | | 277 | | |
| | | 278 | | private bool TryLiteralBranchThenAdvance(ReadOnlyMemory<char> decodedSegment) |
| | 1 | 279 | | { |
| | 1 | 280 | | if (!_node.LiteralBranches.TryGetValue(decodedSegment, out RouteNode literalBranch)) |
| | 1 | 281 | | return false; |
| | | 282 | | |
| | 1 | 283 | | AdvanceToNextSegment(literalBranch); |
| | 1 | 284 | | return true; |
| | 1 | 285 | | } |
| | | 286 | | |
| | | 287 | | private bool TryParsedBranchThenAdvance(KeyValuePair<ParameterParser, RouteNode> parsedBranch, ValueParseResult |
| | 1 | 288 | | { |
| | 1 | 289 | | if (!TryParsedBranch(parsedBranch.Key.Definition, parseResult)) |
| | 1 | 290 | | return false; |
| | | 291 | | |
| | 1 | 292 | | AdvanceToNextSegment(parsedBranch.Value); |
| | 1 | 293 | | return true; |
| | 1 | 294 | | } |
| | | 295 | | #endregion |
| | | 296 | | |
| | 2 | 297 | | public RouteMatchCursor(RouteNode node, HttpVerb verb, Uri uri, IServiceProvider services, IDictionary<string, o |
| | 2 | 298 | | { |
| | 2 | 299 | | _segment = new DelimitedSegment |
| | 2 | 300 | | ( |
| | 2 | 301 | | // Escaped path, not percent decoded -> "/path%2Fto%2Fsomewhere/" will be treated as a single segment |
| | 2 | 302 | | uri.AbsolutePath.AsMemory(), |
| | 2 | 303 | | '/' |
| | 2 | 304 | | ); |
| | 2 | 305 | | _branchOrder = BranchOrder.From(matchingPrecedence); |
| | 2 | 306 | | _root = _node = node; |
| | | 307 | | |
| | 2 | 308 | | Cancellation = cancellation; |
| | 2 | 309 | | Parameters = parameters; |
| | 2 | 310 | | Services = services; |
| | 2 | 311 | | Verb = verb; |
| | | 312 | | |
| | 2 | 313 | | AdvanceToNextSegment(_root); |
| | 2 | 314 | | _phase = GetPhaseForCurrentNode(); |
| | 2 | 315 | | } |
| | | 316 | | |
| | 2 | 317 | | public HandlerRegistration HandlerRegistration { get; private set; } = null!; |
| | | 318 | | |
| | 2 | 319 | | 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 | | |
| | 2 | 329 | | public bool Completed => _phase is MatchPhase.Done; |
| | | 330 | | |
| | | 331 | | public void Dispose() |
| | 2 | 332 | | { |
| | 2 | 333 | | if (_decodedSegmentBuffer is not null) |
| | 1 | 334 | | { |
| | 1 | 335 | | s_arrayPool.Return(_decodedSegmentBuffer, clearArray: false); |
| | 1 | 336 | | _decodedSegmentBuffer = null; |
| | 1 | 337 | | } |
| | 2 | 338 | | } |
| | | 339 | | |
| | | 340 | | public void Reset() |
| | 1 | 341 | | { |
| | 1 | 342 | | HandlerRegistration = null!; |
| | 1 | 343 | | _nextDecodedSegment = 0; |
| | | 344 | | |
| | 1 | 345 | | _segment.Reset(); |
| | 1 | 346 | | AdvanceToNextSegment(_root); |
| | 1 | 347 | | _phase = GetPhaseForCurrentNode(); |
| | 1 | 348 | | } |
| | | 349 | | |
| | | 350 | | public ValueTask<bool> MoveNextAsync() |
| | 2 | 351 | | { |
| | 2 | 352 | | while (!Completed) |
| | 2 | 353 | | { |
| | 2 | 354 | | if (_phase is MatchPhase.EmitHandlers) |
| | 2 | 355 | | { |
| | 2 | 356 | | Cancellation.ThrowIfCancellationRequested(); |
| | | 357 | | |
| | 2 | 358 | | if (TryEmitHandler()) |
| | 2 | 359 | | return s_true; |
| | | 360 | | |
| | | 361 | | // No handler terminated the pipeline, go to the branch matching phase. |
| | 1 | 362 | | _phase = MatchPhase.Branch; |
| | 1 | 363 | | } |
| | | 364 | | |
| | 2 | 365 | | Debug.Assert(_phase is MatchPhase.Branch, $"Unknown phase: {_phase}"); |
| | | 366 | | |
| | 2 | 367 | | if (_segment.HasValue) |
| | 2 | 368 | | { |
| | 2 | 369 | | ValueTask<bool> branchMatched = _node.SingleBranch is not null |
| | 2 | 370 | | // fast path |
| | 2 | 371 | | ? TrySingleBranchesAsync() |
| | 2 | 372 | | : TryBranchPairAsync(); |
| | | 373 | | |
| | 2 | 374 | | if (!branchMatched.IsCompletedSuccessfully) |
| | 1 | 375 | | return MoveNextAwaitedAsync(branchMatched); |
| | | 376 | | |
| | 2 | 377 | | if (branchMatched.Result) |
| | 2 | 378 | | { |
| | 2 | 379 | | _phase = GetPhaseForCurrentNode(); |
| | 2 | 380 | | continue; |
| | | 381 | | } |
| | 1 | 382 | | } |
| | | 383 | | |
| | 1 | 384 | | _phase = MatchPhase.Done; |
| | 1 | 385 | | break; |
| | | 386 | | } |
| | | 387 | | |
| | 1 | 388 | | return s_false; |
| | 2 | 389 | | } |
| | | 390 | | } |
| | | 391 | | } |