File: ApacheModRewrite\TestStringParser.cs
Web Access
Project: src\src\Middleware\Rewrite\src\Microsoft.AspNetCore.Rewrite.csproj (Microsoft.AspNetCore.Rewrite)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using Microsoft.AspNetCore.Rewrite.PatternSegments;
 
namespace Microsoft.AspNetCore.Rewrite.ApacheModRewrite;
 
/// <summary>
/// Parses the TestString segment of the mod_rewrite condition.
/// </summary>
internal sealed class TestStringParser
{
    private const char Percent = '%';
    private const char Dollar = '$';
    private const char Colon = ':';
    private const char OpenBrace = '{';
    private const char CloseBrace = '}';
 
    /// <summary>
    /// Creates a pattern, which is a template to create a new test string to
    /// compare to the condition pattern. Can contain server variables, back references, etc.
    /// </summary>
    /// <param name="testString">The test string portion of the RewriteCond
    /// Examples:
    /// %{REMOTE_ADDR}
    /// /var/www/%{REQUEST_URI}
    /// %1
    /// $1</param>
    /// <returns>A new <see cref="Pattern"/>, containing a list of <see cref="PatternSegment"/></returns>
    /// http://httpd.apache.org/docs/current/mod/mod_rewrite.html
    public static Pattern Parse(string testString)
    {
        if (testString == null)
        {
            testString = string.Empty;
        }
        var context = new ParserContext(testString);
        var results = new List<PatternSegment>();
        while (context.Next())
        {
            switch (context.Current)
            {
                case Percent:
                    // This is a server parameter, parse for a condition variable
                    if (!context.Next())
                    {
                        throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(testString, context.Index));
                    }
                    ParseConditionParameter(context, results);
                    break;
                case Dollar:
                    // This is a parameter from the rule, verify that it is a number from 0 to 9 directly after it
                    // and create a new Pattern Segment.
                    if (!context.Next())
                    {
                        throw new FormatException(Resources.FormatError_InputParserNoBackreference(context.Index));
                    }
                    context.Mark();
                    if (context.Current >= '0' && context.Current <= '9')
                    {
                        context.Next();
                        var ruleVariable = context.Capture()!;
                        context.Back();
                        var parsedIndex = int.Parse(ruleVariable, CultureInfo.InvariantCulture);
 
                        results.Add(new RuleMatchSegment(parsedIndex));
                    }
                    else
                    {
                        throw new FormatException(Resources.FormatError_InputParserInvalidInteger(testString, context.Index));
                    }
                    break;
                default:
                    ParseLiteral(context, results);
                    break;
            }
        }
        return new Pattern(results);
    }
 
    /// <summary>
    /// Obtains the condition parameter, which could either be a condition variable or a
    /// server variable. Assumes the current character is immediately after the '%'.
    /// context, on return will be on the last character of variable captured, such that after
    /// Next() is called, it will be on the character immediately after the condition parameter.
    /// </summary>
    /// <param name="context">The ParserContext</param>
    /// <param name="results">The List of results which the new condition parameter will be added.</param>
    /// <returns>true </returns>
    private static void ParseConditionParameter(ParserContext context, IList<PatternSegment> results)
    {
        // Parse { }
        if (context.Current == OpenBrace)
        {
            // Start of a server variable
            if (!context.Next())
            {
                // Dangling {
                throw new FormatException(Resources.FormatError_InputParserMissingCloseBrace(context.Index));
            }
            context.Mark();
            while (context.Current != CloseBrace)
            {
                if (!context.Next())
                {
                    throw new FormatException(Resources.FormatError_InputParserMissingCloseBrace(context.Index));
                }
                else if (context.Current == Colon)
                {
                    // Have a segmented look up Ex: HTTP:xxxx
                    // Most of these we can't handle
                    throw new NotImplementedException("Segmented Lookups no implemented");
                }
            }
 
            // Need to verify server variable captured exists
            var rawServerVariable = context.Capture()!;
            results.Add(ServerVariables.FindServerVariable(rawServerVariable, context));
        }
        else if (context.Current >= '0' && context.Current <= '9')
        {
            // means we have a segmented lookup
            // store information in the testString result to know what to look up.
            context.Mark();
            context.Next();
            var rawConditionParameter = context.Capture()!;
 
            // Once we leave this method, the while loop will call next again. Because
            // capture is exclusive, we need to go one past the end index, capture, and then go back.
            context.Back();
            var parsedIndex = int.Parse(rawConditionParameter, CultureInfo.InvariantCulture);
            results.Add(new ConditionMatchSegment(parsedIndex));
        }
        else
        {
            // illegal escape of a character
            throw new FormatException(Resources.FormatError_InputParserInvalidInteger(context.Template, context.Index));
        }
    }
 
    /// <summary>
    /// Parse a string literal in the test string. Continues capturing until the start of a new variable type.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="results"></param>
    /// <returns></returns>
    private static void ParseLiteral(ParserContext context, IList<PatternSegment> results)
    {
        context.Mark();
        string? literal;
        while (true)
        {
            if (context.Current == Percent || context.Current == Dollar)
            {
                literal = context.Capture();
                context.Back();
                break;
            }
            if (!context.Next())
            {
                literal = context.Capture();
                break;
            }
        }
        // add results
        results.Add(new LiteralSegment(literal!));
    }
}