File: ContentModel\Infrastructure\Parser.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;

namespace NuGet.ContentModel.Infrastructure
{
    public class PatternExpression
    {
        private readonly List<Segment> _segments = new List<Segment>();
        private readonly Dictionary<string, object> _defaults;
        private readonly PatternTable? _table;

        public PatternExpression(PatternDefinition pattern)
        {
            _table = pattern.Table;
            _defaults = pattern.Defaults.ToDictionary(p => p.Key, p => p.Value);
            Initialize(pattern.Pattern, pattern.PreserveRawValues);
        }

        private void Initialize(string pattern, bool preserveRawValues)
        {
            for (var scanIndex = 0; scanIndex < pattern.Length;)
            {
                var beginToken = pattern.Length;
                var endToken = pattern.Length;
                for (var i = scanIndex; i < pattern.Length; i++)
                {
                    var ch = pattern[i];
                    if (beginToken == pattern.Length)
                    {
                        if (ch == '{')
                        {
                            beginToken = i;
                        }
                    }
                    else if (ch == '}')
                    {
                        endToken = i;
                        break;
                    }
                }

                if (scanIndex != beginToken)
                {
                    _segments.Add(new LiteralSegment(pattern, scanIndex, beginToken - scanIndex));
                }
                if (beginToken != endToken)
                {
                    var delimiter = endToken + 1 < pattern.Length ? pattern[endToken + 1] : '\0';
                    var matchOnly = pattern[endToken - 1] == '?';

                    var beginName = beginToken + 1;
                    var endName = endToken - (matchOnly ? 1 : 0);

                    var tokenName = pattern.Substring(beginName, endName - beginName);
                    _segments.Add(new TokenSegment(tokenName, delimiter, matchOnly, _table, preserveRawValues));
                }
                scanIndex = endToken + 1;
            }
        }

        internal ContentItem? Match(string path, IReadOnlyDictionary<string, ContentPropertyDefinition> propertyDefinitions)
        {
            ContentItem? item = null;
            var startIndex = 0;
            foreach (var segment in _segments)
            {
                int endIndex;
                if (segment.TryMatch(ref item, path, propertyDefinitions, startIndex, out endIndex))
                {
                    startIndex = endIndex;
                    continue;
                }
                return null;
            }

            if (startIndex == path.Length)
            {
                // Successful match!
                // Apply defaults from the pattern
                if (item == null)
                {
                    // item not created, use shared defaults
                    item = new ContentItem
                    {
                        Path = path,
                        Properties = _defaults
                    };
                }
                else
                {
                    // item already created, append defaults
                    foreach (var pair in _defaults)
                    {
                        item.Add(pair.Key, pair.Value);
                    }
                }
                return item;
            }
            return null;
        }

        private abstract class Segment
        {
            internal abstract bool TryMatch(ref ContentItem? item, string path, IReadOnlyDictionary<string, ContentPropertyDefinition> propertyDefinitions, int startIndex, out int endIndex);
        }

        [DebuggerDisplay("{_pattern.Substring(_start, _length)}")]
        private class LiteralSegment : Segment
        {
            private readonly string _pattern;
            private readonly int _start;
            private readonly int _length;

            public LiteralSegment(string pattern, int start, int length)
            {
                _pattern = pattern;
                _start = start;
                _length = length;
            }

            internal override bool TryMatch(
                ref ContentItem? item,
                string path,
                IReadOnlyDictionary<string, ContentPropertyDefinition> propertyDefinitions,
                int startIndex,
                out int endIndex)
            {
                if (path.Length >= startIndex + _length)
                {
                    if (string.Compare(path, startIndex, _pattern, _start, _length, StringComparison.OrdinalIgnoreCase) == 0)
                    {
                        endIndex = startIndex + _length;
                        return true;
                    }
                }
                endIndex = startIndex;
                return false;
            }
        }

        [DebuggerDisplay("Token = {_token}, Delimiter = {_delimiter}, MatchOnly = {_matchOnly}")]
        private class TokenSegment : Segment
        {
            private readonly string _token;
            private readonly char _delimiter;
            private readonly bool _matchOnly;
            private readonly PatternTable? _table;
            private readonly bool _preserveRawValue = false;
            private readonly string? _rawToken;

            public TokenSegment(string token, char delimiter, bool matchOnly, PatternTable? table, bool preserveRawValues)
            {
                _token = token;
                _delimiter = delimiter;
                _matchOnly = matchOnly;
                _table = table;
                _preserveRawValue = preserveRawValues;
                if (_preserveRawValue)
                {
                    _rawToken = $"{token}_raw";
                }
            }

            internal override bool TryMatch(
                ref ContentItem? item,
                string path,
                IReadOnlyDictionary<string, ContentPropertyDefinition> propertyDefinitions,
                int startIndex,
                out int endIndex)
            {
                ContentPropertyDefinition? propertyDefinition;
                if (!propertyDefinitions.TryGetValue(_token, out propertyDefinition))
                {
                    throw new Exception(string.Format(CultureInfo.CurrentCulture, "Unable to find property definition for {{{0}}}", _token));
                }

                for (var scanIndex = startIndex; scanIndex != path.Length;)
                {
                    var delimiterIndex = path.Length;
                    for (var i = scanIndex + 1; i < path.Length; i++)
                    {
                        if (path[i] == _delimiter)
                        {
                            delimiterIndex = i;
                            break;
                        }
                    }

                    if (delimiterIndex == path.Length
                        && _delimiter != '\0')
                    {
                        break;
                    }
                    ReadOnlyMemory<char> substring = path.AsMemory(startIndex, delimiterIndex - startIndex);
                    object? value;
                    if (propertyDefinition.TryLookup(substring, _table, _matchOnly, out value))
                    {
                        if (!_matchOnly)
                        {
                            // Adding property, create item if not already created
                            if (item == null)
                            {
                                item = new ContentItem
                                {
                                    Path = path
                                };
                            }
                            if (_preserveRawValue)
                            {
                                item.Add(_rawToken!, substring.ToString());
                            }
                            item.Add(_token, value!);
                        }
                        endIndex = delimiterIndex;
                        return true;
                    }
                    scanIndex = delimiterIndex;
                }
                endIndex = startIndex;
                return false;
            }
        }
    }
}