File: PathTokenizer.cs
Web Access
Project: src\src\Http\Routing\src\Microsoft.AspNetCore.Routing.csproj (Microsoft.AspNetCore.Routing)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Collections;
using System.Diagnostics;
#if !COMPONENTS
using Microsoft.AspNetCore.Http;
#else
using Microsoft.AspNetCore.Components.Routing;
#endif
using Microsoft.Extensions.Primitives;
 
namespace Microsoft.AspNetCore.Routing;
 
internal struct PathTokenizer : IReadOnlyList<StringSegment>
{
    private readonly string _path;
    private int _count;
 
    public PathTokenizer(PathString path)
    {
        _path = path.Value;
        _count = -1;
    }
 
    public int Count
    {
        get
        {
            if (_count == -1)
            {
                // We haven't computed the real count of segments yet.
                if (_path.Length == 0)
                {
                    // The empty string has length of 0.
                    _count = 0;
                    return _count;
                }
 
                // A string of length 1 must be "/" - all PathStrings start with '/'
                if (_path.Length == 1)
                {
                    // We treat this as empty - there's nothing to parse here for routing, because routing ignores
                    // a trailing slash.
                    Debug.Assert(_path[0] == '/');
                    _count = 0;
                    return _count;
                }
 
                // This is a non-trivial PathString
                _count = 1;
 
                // Since a non-empty PathString must begin with a `/`, we can just count the number of occurrences
                // of `/` to find the number of segments. However, we don't look at the last character, because
                // routing ignores a trailing slash.
                for (var i = 1; i < _path.Length - 1; i++)
                {
                    if (_path[i] == '/')
                    {
                        _count++;
                    }
                }
            }
 
            return _count;
        }
    }
 
    public StringSegment this[int index]
    {
        get
        {
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);
 
            var currentSegmentIndex = 0;
            var currentSegmentStart = 1;
 
            // Skip the first `/`.
            var delimiterIndex = 1;
            while ((delimiterIndex = _path.IndexOf('/', delimiterIndex)) != -1)
            {
                if (currentSegmentIndex++ == index)
                {
                    return new StringSegment(_path, currentSegmentStart, delimiterIndex - currentSegmentStart);
                }
                else
                {
                    currentSegmentStart = delimiterIndex + 1;
                    delimiterIndex++;
                }
            }
 
            // If we get here we're at the end of the string. The implementation of .Count should protect us
            // from these cases.
            Debug.Assert(_path[_path.Length - 1] != '/');
            Debug.Assert(currentSegmentIndex == index);
 
            return new StringSegment(_path, currentSegmentStart, _path.Length - currentSegmentStart);
        }
    }
 
    public Enumerator GetEnumerator()
    {
        return new Enumerator(this);
    }
 
    IEnumerator<StringSegment> IEnumerable<StringSegment>.GetEnumerator()
    {
        return GetEnumerator();
    }
 
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
 
    public struct Enumerator : IEnumerator<StringSegment>
    {
        private readonly string _path;
 
        private int _index;
        private int _length;
 
        public Enumerator(PathTokenizer tokenizer)
        {
            _path = tokenizer._path;
 
            _index = -1;
            _length = -1;
        }
 
        public StringSegment Current
        {
            get
            {
                return new StringSegment(_path, _index, _length);
            }
        }
 
        object IEnumerator.Current
        {
            get
            {
                return Current;
            }
        }
 
        public void Dispose()
        {
        }
 
        public bool MoveNext()
        {
            if (_path == null || _path.Length <= 1)
            {
                return false;
            }
 
            if (_index == -1)
            {
                // Skip the first `/`.
                _index = 1;
            }
            else
            {
                // Skip to the end of the previous segment + the separator.
                _index += _length + 1;
            }
 
            if (_index >= _path.Length)
            {
                // We're at the end
                return false;
            }
 
            var delimiterIndex = _path.IndexOf('/', _index);
            if (delimiterIndex != -1)
            {
                _length = delimiterIndex - _index;
                return true;
            }
 
            // We might have some trailing text after the last separator.
            if (_path[_path.Length - 1] == '/')
            {
                // If the last char is a '/' then it's just a trailing slash, we don't have another segment.
                return false;
            }
            else
            {
                _length = _path.Length - _index;
                return true;
            }
        }
 
        public void Reset()
        {
            _index = -1;
            _length = -1;
        }
    }
}