Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix parsing " #788

Merged
merged 24 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/System.Linq.Dynamic.Core/Config/StringLiteralParsingType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace System.Linq.Dynamic.Core.Config;

/// <summary>
/// Defines the types of string literal parsing that can be performed.
/// </summary>
public enum StringLiteralParsingType : byte
{
/// <summary>
/// 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:
/// <code>
/// var expression = "Value == \"\\\"\"";
/// </code>
/// </summary>
Default = 0,

/// <summary>
/// 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:
/// <code>
/// var expression = "Value == \"\"\"\"";
/// </code>
/// </summary>
EscapeDoubleQuoteByTwoDoubleQuotes = 1
}
12 changes: 10 additions & 2 deletions src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -884,7 +885,7 @@ private AnyOf<Expression, Type> 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] == '\'')
{
Expand Down Expand Up @@ -916,11 +917,18 @@ private AnyOf<Expression, Type> 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);
Expand Down
16 changes: 8 additions & 8 deletions src/System.Linq.Dynamic.Core/Parser/StringParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ namespace System.Linq.Dynamic.Core.Parser;
/// </summary>
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)
{
Expand Down Expand Up @@ -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);
}
}
}
7 changes: 7 additions & 0 deletions src/System.Linq.Dynamic.Core/ParsingConfig.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -273,4 +274,10 @@ public IQueryableAnalyzer QueryableAnalyzer
/// </example>
/// </summary>
public bool ConvertObjectToSupportComparison { get; set; }

/// <summary>
/// Defines the type of string literal parsing that will be performed.
/// Default value is <c>StringLiteralParsingType.Default</c>.
/// </summary>
public StringLiteralParsingType StringLiteralParsing { get; set; } = StringLiteralParsingType.Default;
}
60 changes: 39 additions & 21 deletions test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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<Func<User, string>>)lambdaUserName;

Expand All @@ -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<Func<User, string>>)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<Func<User, bool>>)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
Expand All @@ -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<Func<User, bool>>)lambda;

Expand Down
53 changes: 40 additions & 13 deletions test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ParseException>(() => StringParser.ParseString(input));
var exception = Assert.Throws<ParseException>(() => StringParser.ParseStringAndUnescape(input));

// Assert
Assert.Equal($"Unexpected end of string with unclosed string at position 2 near '{input}'.", exception.Message);
Expand All @@ -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<ParseException>().WithMessage($"String '{input}' should have at least 2 characters.");
Expand All @@ -35,41 +35,41 @@ 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<ParseException>().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>();

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]
[InlineData("''", "")]
[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);
Expand All @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static StaticHelperSqlExpression SubSelect(string columnName, string obje
CustomTypeProvider = new TestCustomTypeProvider()
};

expFilter = DynamicExpressionParser.ParseLambda<User, bool>(config, true, filter); // Failed Here!
expFilter = DynamicExpressionParser.ParseLambda<User, bool>(config, true, filter);
}

return new StaticHelperSqlExpression
Expand Down