diff --git a/src/System.Linq.Dynamic.Core/Config/StringLiteralParsingType.cs b/src/System.Linq.Dynamic.Core/Config/StringLiteralParsingType.cs new file mode 100644 index 00000000..09867a8b --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Config/StringLiteralParsingType.cs @@ -0,0 +1,25 @@ +namespace System.Linq.Dynamic.Core.Config; + +/// +/// Defines the types of string literal parsing that can be performed. +/// +public enum StringLiteralParsingType : byte +{ + /// + /// Represents the default string literal parsing type. Double quotes should be escaped using the default escape character (a \). + /// To check if a Value equals a double quote, use this c# code: + /// + /// var expression = "Value == \"\\\"\""; + /// + /// + Default = 0, + + /// + /// Represents a string literal parsing type where a double quote should be escaped by an extra double quote ("). + /// To check if a Value equals a double quote, use this c# code: + /// + /// var expression = "Value == \"\"\"\""; + /// + /// + EscapeDoubleQuoteByTwoDoubleQuotes = 1 +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index a5154e23..50ae95c6 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq.Dynamic.Core.Config; using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Dynamic.Core.Extensions; using System.Linq.Dynamic.Core.Parser.SupportedMethods; @@ -884,7 +885,7 @@ private AnyOf ParseStringLiteral(bool forceParseAsString) _textParser.ValidateToken(TokenId.StringLiteral); var text = _textParser.CurrentToken.Text; - var parsedStringValue = StringParser.ParseString(_textParser.CurrentToken.Text); + var parsedStringValue = ParseStringAndEscape(text); if (_textParser.CurrentToken.Text[0] == '\'') { @@ -916,11 +917,18 @@ private AnyOf ParseStringLiteral(bool forceParseAsString) _textParser.NextToken(); } - parsedStringValue = StringParser.ParseStringAndReplaceDoubleQuotes(text, _textParser.CurrentToken.Pos); + parsedStringValue = ParseStringAndEscape(text); return _constantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue); } + private string ParseStringAndEscape(string text) + { + return _parsingConfig.StringLiteralParsing == StringLiteralParsingType.EscapeDoubleQuoteByTwoDoubleQuotes ? + StringParser.ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(text, _textParser.CurrentToken.Pos) : + StringParser.ParseStringAndUnescape(text, _textParser.CurrentToken.Pos); + } + private Expression ParseIntegerLiteral() { _textParser.ValidateToken(TokenId.IntegerLiteral); diff --git a/src/System.Linq.Dynamic.Core/Parser/StringParser.cs b/src/System.Linq.Dynamic.Core/Parser/StringParser.cs index 18dcb2b2..99aea158 100644 --- a/src/System.Linq.Dynamic.Core/Parser/StringParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/StringParser.cs @@ -10,10 +10,10 @@ namespace System.Linq.Dynamic.Core.Parser; /// internal static class StringParser { - private const string Pattern = @""""""; - private const string Replacement = "\""; + private const string TwoDoubleQuotes = "\"\""; + private const string SingleDoubleQuote = "\""; - public static string ParseString(string s, int pos = default) + internal static string ParseStringAndUnescape(string s, int pos = default) { if (s == null || s.Length < 2) { @@ -41,20 +41,20 @@ public static string ParseString(string s, int pos = default) } } - public static string ParseStringAndReplaceDoubleQuotes(string s, int pos) + internal static string ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(string input, int position = default) { - return ReplaceDoubleQuotes(ParseString(s, pos), pos); + return ReplaceTwoDoubleQuotesByASingleDoubleQuote(ParseStringAndUnescape(input, position), position); } - private static string ReplaceDoubleQuotes(string s, int pos) + private static string ReplaceTwoDoubleQuotesByASingleDoubleQuote(string input, int position) { try { - return Regex.Replace(s, Pattern, Replacement); + return Regex.Replace(input, TwoDoubleQuotes, SingleDoubleQuote); } catch (Exception ex) { - throw new ParseException(ex.Message, pos, ex); + throw new ParseException(ex.Message, position, ex); } } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs index 883c71ea..3c80b7bb 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Globalization; +using System.Linq.Dynamic.Core.Config; using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Parser; using System.Linq.Dynamic.Core.Util.Cache; @@ -273,4 +274,10 @@ public IQueryableAnalyzer QueryableAnalyzer /// /// public bool ConvertObjectToSupportComparison { get; set; } + + /// + /// Defines the type of string literal parsing that will be performed. + /// Default value is StringLiteralParsingType.Default. + /// + public StringLiteralParsingType StringLiteralParsing { get; set; } = StringLiteralParsingType.Default; } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index 4c4a9ee0..055216a1 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Globalization; +using System.Linq.Dynamic.Core.Config; using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Dynamic.Core.Tests.Helpers; @@ -975,6 +976,21 @@ public void DynamicExpressionParser_ParseLambda_StringLiteralStartEmbeddedQuote_ Assert.Equal("\"\"test\"", rightValue); } + [Theory] // #786 + [InlineData("Escaped", "\"{\\\"PropertyA\\\":\\\"\\\"}\"")] + [InlineData("Verbatim", @"""{\""PropertyA\"":\""\""}""")] + // [InlineData("Raw", """"{\"PropertyA\":\"\"}"""")] // TODO : does not work ??? + public void DynamicExpressionParser_ParseLambda_StringLiteral_EscapedJson(string _, string expression) + { + // Act + var result = DynamicExpressionParser + .ParseLambda(typeof(object), expression) + .Compile() + .DynamicInvoke(); + + result.Should().Be("{\"PropertyA\":\"\"}"); + } + [Fact] public void DynamicExpressionParser_ParseLambda_StringLiteral_MissingClosingQuote() { @@ -1549,7 +1565,10 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio resultIncome.Should().Be("Income == 5"); // Act : string - var expressionTextUserName = "StaticHelper.Filter(\"UserName == \"\"x\"\"\")"; + // Replace " with \" + // Replace \" with \\\" + StaticHelper.Filter("UserName == \"x\""); + var expressionTextUserName = "StaticHelper.Filter(\"UserName == \\\"x\\\"\")"; var lambdaUserName = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionTextUserName, user); var funcUserName = (Expression>)lambdaUserName; @@ -1558,33 +1577,28 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio // Assert : string resultUserName.Should().Be(@"UserName == ""x"""); - } - [Fact] - public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpression1String() - { - // Arrange - var config = new ParsingConfig + // Act : string + // Replace " with \" + // Replace \" with \"\" + var configNonDefault = new ParsingConfig { - CustomTypeProvider = new TestCustomTypeProvider() + CustomTypeProvider = new TestCustomTypeProvider(), + StringLiteralParsing = StringLiteralParsingType.EscapeDoubleQuoteByTwoDoubleQuotes }; + expressionTextUserName = "StaticHelper.Filter(\"UserName == \"\"x\"\"\")"; + lambdaUserName = DynamicExpressionParser.ParseLambda(configNonDefault, typeof(User), null, expressionTextUserName, user); + funcUserName = (Expression>)lambdaUserName; - var user = new User(); + delegateUserName = funcUserName.Compile(); + resultUserName = (string?)delegateUserName.DynamicInvoke(user); - // Act - var expressionText = @"StaticHelper.In(Id, StaticHelper.SubSelect(""Identity"", ""LegalPerson"", ""StaticHelper.In(ParentId, StaticHelper.SubSelect(""""LegalPersonId"""", """"PointSiteTD"""", """"Identity = 5"""", """"""""))"", """"))"; - var lambda = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionText, user); - var func = (Expression>)lambda; - - var compile = func.Compile(); - var result = (bool?)compile.DynamicInvoke(user); - - // Assert - result.Should().Be(false); + // Assert : string + resultUserName.Should().Be(@"UserName == ""x"""); } [Fact] - public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpression2String() + public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpressionString() { // Arrange var config = new ParsingConfig @@ -1594,8 +1608,12 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexEx var user = new User(); + // Replace " with \" + // Replace \" with \\\" + var _ = StaticHelper.In(Guid.NewGuid(), StaticHelper.SubSelect("Identity", "LegalPerson", "StaticHelper.In(ParentId, StaticHelper.SubSelect( \"LegalPersonId\", \"PointSiteTD\", \"Identity = 5\", \"\")) ", "")); + var expressionText = "StaticHelper.In(Id, StaticHelper.SubSelect(\"Identity\", \"LegalPerson\", \"StaticHelper.In(ParentId, StaticHelper.SubSelect(\\\"LegalPersonId\\\", \\\"PointSiteTD\\\", \\\"Identity = 5\\\", \\\"\\\"))\", \"\"))"; + // Act - var expressionText = @"StaticHelper.In(Id, StaticHelper.SubSelect(""Identity"", ""LegalPerson"", ""StaticHelper.In(ParentId, StaticHelper.SubSelect(""""LegalPersonId"""", """"PointSiteTD"""", """"Identity = "" + StaticHelper.ToExpressionString(StaticHelper.Get(""CurrentPlace""), 2) + """""", """"""""))"", """"))"; var lambda = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionText, user); var func = (Expression>)lambda; diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs index b39a2349..0272f026 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs @@ -10,10 +10,10 @@ public class StringParserTests [Theory] [InlineData("'s")] [InlineData("\"s")] - public void StringParser_With_UnexpectedUnclosedString_ThrowsException(string input) + public void StringParser_ParseStringAndUnescape_With_UnexpectedUnclosedString_ThrowsException(string input) { // Act - var exception = Assert.Throws(() => StringParser.ParseString(input)); + var exception = Assert.Throws(() => StringParser.ParseStringAndUnescape(input)); // Assert Assert.Equal($"Unexpected end of string with unclosed string at position 2 near '{input}'.", exception.Message); @@ -23,10 +23,10 @@ public void StringParser_With_UnexpectedUnclosedString_ThrowsException(string in [InlineData("")] [InlineData(null)] [InlineData("x")] - public void StringParser_With_InvalidStringLength_ThrowsException(string input) + public void StringParser_ParseStringAndUnescape_With_InvalidStringLength_ThrowsException(string input) { // Act - Action action = () => StringParser.ParseString(input); + Action action = () => StringParser.ParseStringAndUnescape(input); // Assert action.Should().Throw().WithMessage($"String '{input}' should have at least 2 characters."); @@ -35,30 +35,30 @@ public void StringParser_With_InvalidStringLength_ThrowsException(string input) [Theory] [InlineData("xx")] [InlineData(" ")] - public void StringParser_With_InvalidStringQuoteCharacter_ThrowsException(string input) + public void StringParser_ParseStringAndUnescape_With_InvalidStringQuoteCharacter_ThrowsException(string input) { // Act - Action action = () => StringParser.ParseString(input); + Action action = () => StringParser.ParseStringAndUnescape(input); // Assert action.Should().Throw().WithMessage("An escaped string should start with a double (\") or a single (') quote."); } [Fact] - public void StringParser_With_UnexpectedUnrecognizedEscapeSequence_ThrowsException() + public void StringParser_ParseStringAndUnescape_With_UnexpectedUnrecognizedEscapeSequence_ThrowsException() { // Arrange var input = new string(new[] { '"', '\\', 'u', '?', '"' }); // Act - Action action = () => StringParser.ParseString(input); + Action action = () => StringParser.ParseStringAndUnescape(input); // Assert var parseException = action.Should().Throw(); parseException.Which.InnerException!.Message.Should().Contain("hexadecimal digits"); - parseException.Which.StackTrace.Should().Contain("at System.Linq.Dynamic.Core.Parser.StringParser.ParseString(String s, Int32 pos) in ").And.Contain("StringParser.cs:line "); + parseException.Which.StackTrace.Should().Contain("at System.Linq.Dynamic.Core.Parser.StringParser.ParseStringAndUnescape(String s, Int32 pos) in ").And.Contain("StringParser.cs:line "); } [Theory] @@ -66,10 +66,10 @@ public void StringParser_With_UnexpectedUnrecognizedEscapeSequence_ThrowsExcepti [InlineData("'s'", "s")] [InlineData("'\\\\'", "\\")] [InlineData("'\\n'", "\n")] - public void StringParser_Parse_SingleQuotedString(string input, string expectedResult) + public void StringParser_ParseStringAndUnescape_SingleQuotedString(string input, string expectedResult) { // Act - var result = StringParser.ParseString(input); + var result = StringParser.ParseStringAndUnescape(input); // Assert result.Should().Be(expectedResult); @@ -93,12 +93,39 @@ public void StringParser_Parse_SingleQuotedString(string input, string expectedR [InlineData("\"\\\"\\\"\"", "\"\"")] [InlineData("\"AB YZ 19 \uD800\udc05 \u00e4\"", "AB YZ 19 \uD800\udc05 \u00e4")] [InlineData("\"\\\\\\\\192.168.1.1\\\\audio\\\\new\"", "\\\\192.168.1.1\\audio\\new")] - public void StringParser_Parse_DoubleQuotedString(string input, string expectedResult) + [InlineData("\"{\\\"PropertyA\\\":\\\"\\\"}\"", @"{""PropertyA"":""""}")] // #786 + public void StringParser_ParseStringAndUnescape_DoubleQuotedString(string input, string expectedResult) { // Act - var result = StringParser.ParseString(input); + var result = StringParser.ParseStringAndUnescape(input); // Assert result.Should().Be(expectedResult); } + + [Fact] + public void StringParser_ParseStringAndUnescape() + { + // Arrange + var test = "\"x\\\"X\""; + + // Act + var result = StringParser.ParseStringAndUnescape(test); + + // Assert + result.Should().Be("x\"X"); + } + + [Fact] + public void StringParser_ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote() + { + // Arrange + var test = "\"x\"\"X\""; + + // Act + var result = StringParser.ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(test); + + // Assert + result.Should().Be("x\"X"); + } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs b/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs index 7bfd3b46..69fc5ce3 100644 --- a/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs +++ b/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs @@ -37,7 +37,7 @@ public static StaticHelperSqlExpression SubSelect(string columnName, string obje CustomTypeProvider = new TestCustomTypeProvider() }; - expFilter = DynamicExpressionParser.ParseLambda(config, true, filter); // Failed Here! + expFilter = DynamicExpressionParser.ParseLambda(config, true, filter); } return new StaticHelperSqlExpression