diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/FlagParser.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/FlagParser.cs index 16a0900815..d06085df59 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/FlagParser.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/FlagParser.cs @@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.ModRewrite FlagType flag; if (!_ruleFlagLookup.TryGetValue(hasPayload[0], out flag)) { - throw new FormatException($"Unrecognized flag: {hasPayload[0]}"); + throw new FormatException($"Unrecognized flag: '{hasPayload[0]}'"); } if (hasPayload.Length == 2) diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/TestStringParser.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/TestStringParser.cs index bcedbb214f..787ef16e7c 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/TestStringParser.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/TestStringParser.cs @@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.ModRewrite else { // illegal escape of a character - throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index)); + throw new FormatException(Resources.FormatError_InputParserInvalidInteger(context.Template, context.Index)); } } diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/Tokenizer.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/Tokenizer.cs index 69c7880f50..2546f6e059 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/Tokenizer.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/ModRewrite/Tokenizer.cs @@ -29,27 +29,19 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.ModRewrite return null; } var context = new ParserContext(rule); - if (!context.Next()) - { - return null; - } - + context.Next(); + var tokens = new List(); context.Mark(); while (true) { - if (!context.Next()) - { - // End of string. Capture. - break; - } - else if (context.Current == Escape) + if (context.Current == Escape) { // Need to progress such that the next character is not evaluated. if (!context.Next()) { // Means that a character was not escaped appropriately Ex: "foo\" - throw new FormatException("Invalid escaper character in string " + rule); + throw new FormatException($"Invalid escaper character in string: {rule}"); } } else if (context.Current == Space || context.Current == Tab) @@ -68,14 +60,21 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.ModRewrite } } context.Mark(); + context.Back(); } } + if (!context.Next()) + { + // End of string. Capture. + break; + } } var done = context.Capture(); if (!string.IsNullOrEmpty(done)) { tokens.Add(done); } + return tokens; } } diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/ConditionActionTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/ConditionPatternParserTest.cs similarity index 70% rename from test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/ConditionActionTest.cs rename to test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/ConditionPatternParserTest.cs index c1a47b26da..7d67317e1b 100644 --- a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/ConditionActionTest.cs +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/ConditionPatternParserTest.cs @@ -1,12 +1,13 @@ // 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 Microsoft.AspNetCore.Rewrite.Internal.ModRewrite; using Xunit; namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite { - public class ConditionActionTest + public class ConditionPatternParserTest { [Theory] [InlineData(">hey", OperationType.Greater, "hey", ConditionType.StringComp)] @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite [InlineData(">=hey", OperationType.GreaterEqual, "hey", ConditionType.StringComp)] [InlineData("<=hey", OperationType.LessEqual, "hey", ConditionType.StringComp)] [InlineData("=hey", OperationType.Equal, "hey", ConditionType.StringComp)] - public void ConditionParser_CheckStringComp(string condition, OperationType operation, string variable, ConditionType conditionType) + public void ConditionPatternParser_CheckStringComp(string condition, OperationType operation, string variable, ConditionType conditionType) { var results = new ConditionPatternParser().ParseActionCondition(condition); @@ -23,12 +24,12 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite } [Fact] - public void ConditionParser_CheckRegexEqual() + public void ConditionPatternParser_CheckRegexEqual() { var condition = @"(.*)"; var results = new ConditionPatternParser().ParseActionCondition(condition); - var expected = new ParsedModRewriteInput { ConditionType = ConditionType.Regex, Operand = "(.*)", Invert = false }; + var expected = new ParsedModRewriteInput { ConditionType = ConditionType.Regex, Operand = "(.*)", Invert = false }; Assert.True(CompareConditions(results, expected)); } @@ -42,11 +43,11 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite [InlineData("-s", OperationType.Size, ConditionType.PropertyTest)] [InlineData("-U", OperationType.ExistingUrl, ConditionType.PropertyTest)] [InlineData("-x", OperationType.Executable, ConditionType.PropertyTest)] - public void ConditionParser_CheckFileOperations(string condition, OperationType operation, ConditionType cond) + public void ConditionPatternParser_CheckFileOperations(string condition, OperationType operation, ConditionType cond) { var results = new ConditionPatternParser().ParseActionCondition(condition); - var expected = new ParsedModRewriteInput { ConditionType = cond, OperationType = operation , Invert = false }; + var expected = new ParsedModRewriteInput { ConditionType = cond, OperationType = operation, Invert = false }; Assert.True(CompareConditions(results, expected)); } @@ -60,7 +61,7 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite [InlineData("!-s", OperationType.Size, ConditionType.PropertyTest)] [InlineData("!-U", OperationType.ExistingUrl, ConditionType.PropertyTest)] [InlineData("!-x", OperationType.Executable, ConditionType.PropertyTest)] - public void ConditionParser_CheckFileOperationsInverted(string condition, OperationType operation, ConditionType cond) + public void ConditionPatternParser_CheckFileOperationsInverted(string condition, OperationType operation, ConditionType cond) { var results = new ConditionPatternParser().ParseActionCondition(condition); @@ -75,7 +76,7 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite [InlineData("-le1", OperationType.LessEqual, "1", ConditionType.IntComp)] [InlineData("-eq1", OperationType.Equal, "1", ConditionType.IntComp)] [InlineData("-ne1", OperationType.NotEqual, "1", ConditionType.IntComp)] - public void ConditionParser_CheckIntComp(string condition, OperationType operation, string variable, ConditionType cond) + public void ConditionPatternParser_CheckIntComp(string condition, OperationType operation, string variable, ConditionType cond) { var results = new ConditionPatternParser().ParseActionCondition(condition); @@ -83,7 +84,22 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite Assert.True(CompareConditions(results, expected)); } - // TODO negative tests + [Theory] + [InlineData("", "Unrecognized parameter type: '', terminated at string index: '0'")] + [InlineData("!", "Unrecognized parameter type: '!', terminated at string index: '1'")] + [InlineData(">", "Unrecognized parameter type: '>', terminated at string index: '1'")] + [InlineData("<", "Unrecognized parameter type: '<', terminated at string index: '1'")] + [InlineData("=", "Unrecognized parameter type: '=', terminated at string index: '1'")] + [InlineData(">=", "Unrecognized parameter type: '>=', terminated at string index: '2'")] + [InlineData("<=", "Unrecognized parameter type: '<=', terminated at string index: '2'")] + [InlineData("-a", "Unrecognized parameter type: '-a', terminated at string index: '1'")] + [InlineData("-gewow", "Unrecognized parameter type: '-gewow', terminated at string index: '3'")] + public void ConditionPatternParser_AssertBadInputThrowsFormatException(string input, string expected) + { + var ex = Assert.Throws(() => new ConditionPatternParser().ParseActionCondition(input)); + Assert.Equal(expected, ex.Message); + } + private bool CompareConditions(ParsedModRewriteInput i1, ParsedModRewriteInput i2) { if (i1.OperationType != i2.OperationType || diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/FlagParserTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/FlagParserTest.cs index 1cc8aa9a6f..d788fa25b9 100644 --- a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/FlagParserTest.cs +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/FlagParserTest.cs @@ -1,6 +1,7 @@ // 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.Linq; using Microsoft.AspNetCore.Rewrite.Internal.ModRewrite; @@ -47,6 +48,24 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite Assert.True(DictionaryContentsEqual(results.FlagDictionary, expected.FlagDictionary)); } + [Theory] + [InlineData("]", "Flags should start and end with square brackets: [flags]")] + [InlineData("[", "Flags should start and end with square brackets: [flags]")] + [InlineData("[R, L]", "Unrecognized flag: ' L'")] // cannot have spaces after , + [InlineData("[RL]", "Unrecognized flag: 'RL'")] + public void FlagParser_AssertFormatErrorWhenFlagsArePoorlyConstructed(string input, string expected) + { + var ex = Assert.Throws(() => new FlagParser().Parse(input)); + Assert.Equal(expected, ex.Message); + } + + [Fact] + public void FlagParser_AssertArgumentExceptionWhenFlagsAreNullOrEmpty() + { + Assert.Throws(() => new FlagParser().Parse(null)); + Assert.Throws(() => new FlagParser().Parse(string.Empty)); + } + public bool DictionaryContentsEqual(IDictionary dictionary, IDictionary other) { return (other ?? new Dictionary()) diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/FormatExceptionTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/FormatExceptionTests.cs index 73d9f3c4c6..6c0913565a 100644 --- a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/FormatExceptionTests.cs +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/FormatExceptionTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite public class FormatExceptionTests { [Theory] - [InlineData(@"RewriteCond 1 2\", @"Invalid escaper character in string RewriteCond 1 2\")] + [InlineData(@"RewriteCond 1 2\", @"Invalid escaper character in string: RewriteCond 1 2\")] [InlineData("BadExpression 1 2 3 4", "Could not parse the mod_rewrite file. Message: 'Too many tokens on line'. Line number '1'.")] [InlineData("RewriteCond % 2", "Could not parse the mod_rewrite file. Line number '1'.")] [InlineData("RewriteCond %{ 2", "Could not parse the mod_rewrite file. Line number '1'.")] diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/RewriteTokenizerTest.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/RewriteTokenizerTest.cs index 7b8b5a74f5..4c211f4bae 100644 --- a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/RewriteTokenizerTest.cs +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/RewriteTokenizerTest.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Rewrite.Internal.ModRewrite; using Xunit; @@ -31,9 +32,45 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.ModRewrite var expected = new List(); expected.Add("RewriteCond"); - expected.Add(@"%{HTTPS}\ what"); // TODO maybe just have the space here? talking point + expected.Add(@"%{HTTPS}\ what"); expected.Add("!-f"); - Assert.Equal(tokens,expected); + Assert.Equal(tokens, expected); + } + + [Fact] + public void Tokenize_CheckWhiteSpaceDirectlyFollowedByEscapeCharacter_CorrectSplit() + { + var testString = @"RewriteCond %{HTTPS} \ what !-f"; + var tokens = new Tokenizer().Tokenize(testString); + + var expected = new List(); + expected.Add(@"RewriteCond"); + expected.Add(@"%{HTTPS}"); + expected.Add(@"\ what"); + expected.Add(@"!-f"); + Assert.Equal(tokens, expected); + } + + [Fact] + public void Tokenize_CheckWhiteSpaceAtEndOfString_CorrectSplit() + { + var testString = @"RewriteCond %{HTTPS} \ what !-f "; + var tokens = new Tokenizer().Tokenize(testString); + + var expected = new List(); + expected.Add(@"RewriteCond"); + expected.Add(@"%{HTTPS}"); + expected.Add(@"\ what"); + expected.Add(@"!-f"); + Assert.Equal(expected, tokens); + } + + [Fact] + public void Tokenize_AssertFormatExceptionWhenEscapeCharacterIsAtEndOfString() + { + + var ex = Assert.Throws(() => new Tokenizer().Tokenize("\\")); + Assert.Equal(@"Invalid escaper character in string: \", ex.Message); } } } diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/TestStringParserTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/TestStringParserTests.cs new file mode 100644 index 0000000000..a362137d10 --- /dev/null +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/ModRewrite/TestStringParserTests.cs @@ -0,0 +1,141 @@ +// 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 Microsoft.AspNetCore.Rewrite.Internal; +using Microsoft.AspNetCore.Rewrite.Internal.ModRewrite; +using Microsoft.AspNetCore.Rewrite.Internal.PatternSegments; +using Xunit; + +namespace Microsoft.AspNetCore.Rewrite +{ + public class TestStringParserTests + { + [Fact] + public void ConditionParser_SingleServerVariable() + { + var serverVar = "%{HTTPS}"; + + var result = new TestStringParser().Parse(serverVar); + + var list = new List(); + list.Add(new IsHttpsModSegment()); + var expected = new Pattern(list); + AssertPatternsEqual(expected, result); + } + + [Fact] + public void ConditionParser_MultipleServerVariables() + { + var serverVar = "%{HTTPS}%{REQUEST_URI}"; + var result = new TestStringParser().Parse(serverVar); + + var list = new List(); + list.Add(new IsHttpsModSegment()); + list.Add(new UrlSegment()); + var expected = new Pattern(list); + AssertPatternsEqual(expected, result); + } + + [Fact] + public void ConditionParser_ParseLiteral() + { + var serverVar = "Hello!"; + var result = new TestStringParser().Parse(serverVar); + + var list = new List(); + list.Add(new LiteralSegment(serverVar)); + var expected = new Pattern(list); + AssertPatternsEqual(expected, result); + } + + [Fact] + public void ConditionParser_ParseConditionParameters() + { + var serverVar = "%1"; + var result = new TestStringParser().Parse(serverVar); + + var list = new List(); + list.Add(new ConditionMatchSegment(1)); + var expected = new Pattern(list); + AssertPatternsEqual(expected, result); + } + + [Fact] + public void ConditionParser_ParseMultipleConditionParameters() + { + var serverVar = "%1%2"; + var result = new TestStringParser().Parse(serverVar); + + var list = new List(); + list.Add(new ConditionMatchSegment(1)); + list.Add(new ConditionMatchSegment(2)); + var expected = new Pattern(list); + AssertPatternsEqual(expected, result); + } + + [Fact] + public void ConditionParser_ParseRuleVariable() + { + var serverVar = "$1"; + var result = new TestStringParser().Parse(serverVar); + + var list = new List(); + list.Add(new RuleMatchSegment(1)); + var expected = new Pattern(list); + AssertPatternsEqual(expected, result); + } + [Fact] + public void ConditionParser_ParseMultipleRuleVariables() + { + var serverVar = "$1$2"; + var result = new TestStringParser().Parse(serverVar); + + var list = new List(); + list.Add(new RuleMatchSegment(1)); + list.Add(new RuleMatchSegment(2)); + var expected = new Pattern(list); + AssertPatternsEqual(expected, result); + } + + [Fact] + public void ConditionParser_ParserComplexRequest() + { + var serverVar = "%{HTTPS}/$1"; + var result = new TestStringParser().Parse(serverVar); + + var list = new List(); + list.Add(new IsHttpsModSegment()); + list.Add(new LiteralSegment("/")); + list.Add(new RuleMatchSegment(1)); + var expected = new Pattern(list); + AssertPatternsEqual(expected, result); + } + + [Theory] + [InlineData(@"%}", "Cannot parse '%}' to integer at string index: '1'")] // no } at end + [InlineData(@"%{", "Missing close brace for parameter at string index: '2'")] // no closing } + [InlineData(@"%a", "Cannot parse '%a' to integer at string index: '1'")] // invalid character after % + [InlineData(@"$a", "Cannot parse '$a' to integer at string index: '1'")] // invalid character after $ + [InlineData(@"%{asdf", "Missing close brace for parameter at string index: '6'")] // no closing } with characters + public void ConditionParser_Bad(string testString, string expected) + { + var ex = Assert.Throws(() => new TestStringParser().Parse(testString)); + Assert.Equal(ex.Message, expected); + } + + private void AssertPatternsEqual(Pattern p1, Pattern p2) + { + Assert.Equal(p1.PatternSegments.Count, p2.PatternSegments.Count); + + for (int i = 0; i < p1.PatternSegments.Count; i++) + { + var s1 = p1.PatternSegments[i]; + var s2 = p2.PatternSegments[i]; + + Assert.Equal(s1.GetType(), s2.GetType()); + } + } + } +}