Skip to content

Commit

Permalink
Fix parsing " (#788)
Browse files Browse the repository at this point in the history
* .

* .

* ....

* t

* p

* rename

* \" ?

* StringParser_ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote

* fix

* .
  • Loading branch information
StefH authored Jul 1, 2024
1 parent 4e6c8c8 commit c404024
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 45 deletions.
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

0 comments on commit c404024

Please sign in to comment.