Skip to content

Commit c404024

Browse files
authored
Fix parsing " (#788)
* . * . * .... * t * p * rename * \" ? * StringParser_ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote * fix * .
1 parent 4e6c8c8 commit c404024

File tree

7 files changed

+130
-45
lines changed

7 files changed

+130
-45
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace System.Linq.Dynamic.Core.Config;
2+
3+
/// <summary>
4+
/// Defines the types of string literal parsing that can be performed.
5+
/// </summary>
6+
public enum StringLiteralParsingType : byte
7+
{
8+
/// <summary>
9+
/// Represents the default string literal parsing type. Double quotes should be escaped using the default escape character (a \).
10+
/// To check if a Value equals a double quote, use this c# code:
11+
/// <code>
12+
/// var expression = "Value == \"\\\"\"";
13+
/// </code>
14+
/// </summary>
15+
Default = 0,
16+
17+
/// <summary>
18+
/// Represents a string literal parsing type where a double quote should be escaped by an extra double quote (").
19+
/// To check if a Value equals a double quote, use this c# code:
20+
/// <code>
21+
/// var expression = "Value == \"\"\"\"";
22+
/// </code>
23+
/// </summary>
24+
EscapeDoubleQuoteByTwoDoubleQuotes = 1
25+
}

src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.ComponentModel;
44
using System.Diagnostics.CodeAnalysis;
55
using System.Globalization;
6+
using System.Linq.Dynamic.Core.Config;
67
using System.Linq.Dynamic.Core.Exceptions;
78
using System.Linq.Dynamic.Core.Extensions;
89
using System.Linq.Dynamic.Core.Parser.SupportedMethods;
@@ -884,7 +885,7 @@ private AnyOf<Expression, Type> ParseStringLiteral(bool forceParseAsString)
884885
_textParser.ValidateToken(TokenId.StringLiteral);
885886

886887
var text = _textParser.CurrentToken.Text;
887-
var parsedStringValue = StringParser.ParseString(_textParser.CurrentToken.Text);
888+
var parsedStringValue = ParseStringAndEscape(text);
888889

889890
if (_textParser.CurrentToken.Text[0] == '\'')
890891
{
@@ -916,11 +917,18 @@ private AnyOf<Expression, Type> ParseStringLiteral(bool forceParseAsString)
916917
_textParser.NextToken();
917918
}
918919

919-
parsedStringValue = StringParser.ParseStringAndReplaceDoubleQuotes(text, _textParser.CurrentToken.Pos);
920+
parsedStringValue = ParseStringAndEscape(text);
920921

921922
return _constantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue);
922923
}
923924

925+
private string ParseStringAndEscape(string text)
926+
{
927+
return _parsingConfig.StringLiteralParsing == StringLiteralParsingType.EscapeDoubleQuoteByTwoDoubleQuotes ?
928+
StringParser.ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(text, _textParser.CurrentToken.Pos) :
929+
StringParser.ParseStringAndUnescape(text, _textParser.CurrentToken.Pos);
930+
}
931+
924932
private Expression ParseIntegerLiteral()
925933
{
926934
_textParser.ValidateToken(TokenId.IntegerLiteral);

src/System.Linq.Dynamic.Core/Parser/StringParser.cs

+8-8
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ namespace System.Linq.Dynamic.Core.Parser;
1010
/// </summary>
1111
internal static class StringParser
1212
{
13-
private const string Pattern = @"""""";
14-
private const string Replacement = "\"";
13+
private const string TwoDoubleQuotes = "\"\"";
14+
private const string SingleDoubleQuote = "\"";
1515

16-
public static string ParseString(string s, int pos = default)
16+
internal static string ParseStringAndUnescape(string s, int pos = default)
1717
{
1818
if (s == null || s.Length < 2)
1919
{
@@ -41,20 +41,20 @@ public static string ParseString(string s, int pos = default)
4141
}
4242
}
4343

44-
public static string ParseStringAndReplaceDoubleQuotes(string s, int pos)
44+
internal static string ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(string input, int position = default)
4545
{
46-
return ReplaceDoubleQuotes(ParseString(s, pos), pos);
46+
return ReplaceTwoDoubleQuotesByASingleDoubleQuote(ParseStringAndUnescape(input, position), position);
4747
}
4848

49-
private static string ReplaceDoubleQuotes(string s, int pos)
49+
private static string ReplaceTwoDoubleQuotesByASingleDoubleQuote(string input, int position)
5050
{
5151
try
5252
{
53-
return Regex.Replace(s, Pattern, Replacement);
53+
return Regex.Replace(input, TwoDoubleQuotes, SingleDoubleQuote);
5454
}
5555
catch (Exception ex)
5656
{
57-
throw new ParseException(ex.Message, pos, ex);
57+
throw new ParseException(ex.Message, position, ex);
5858
}
5959
}
6060
}

src/System.Linq.Dynamic.Core/ParsingConfig.cs

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Generic;
22
using System.ComponentModel;
33
using System.Globalization;
4+
using System.Linq.Dynamic.Core.Config;
45
using System.Linq.Dynamic.Core.CustomTypeProviders;
56
using System.Linq.Dynamic.Core.Parser;
67
using System.Linq.Dynamic.Core.Util.Cache;
@@ -273,4 +274,10 @@ public IQueryableAnalyzer QueryableAnalyzer
273274
/// </example>
274275
/// </summary>
275276
public bool ConvertObjectToSupportComparison { get; set; }
277+
278+
/// <summary>
279+
/// Defines the type of string literal parsing that will be performed.
280+
/// Default value is <c>StringLiteralParsingType.Default</c>.
281+
/// </summary>
282+
public StringLiteralParsingType StringLiteralParsing { get; set; } = StringLiteralParsingType.Default;
276283
}

test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs

+39-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.Globalization;
3+
using System.Linq.Dynamic.Core.Config;
34
using System.Linq.Dynamic.Core.CustomTypeProviders;
45
using System.Linq.Dynamic.Core.Exceptions;
56
using System.Linq.Dynamic.Core.Tests.Helpers;
@@ -975,6 +976,21 @@ public void DynamicExpressionParser_ParseLambda_StringLiteralStartEmbeddedQuote_
975976
Assert.Equal("\"\"test\"", rightValue);
976977
}
977978

979+
[Theory] // #786
980+
[InlineData("Escaped", "\"{\\\"PropertyA\\\":\\\"\\\"}\"")]
981+
[InlineData("Verbatim", @"""{\""PropertyA\"":\""\""}""")]
982+
// [InlineData("Raw", """"{\"PropertyA\":\"\"}"""")] // TODO : does not work ???
983+
public void DynamicExpressionParser_ParseLambda_StringLiteral_EscapedJson(string _, string expression)
984+
{
985+
// Act
986+
var result = DynamicExpressionParser
987+
.ParseLambda(typeof(object), expression)
988+
.Compile()
989+
.DynamicInvoke();
990+
991+
result.Should().Be("{\"PropertyA\":\"\"}");
992+
}
993+
978994
[Fact]
979995
public void DynamicExpressionParser_ParseLambda_StringLiteral_MissingClosingQuote()
980996
{
@@ -1549,7 +1565,10 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio
15491565
resultIncome.Should().Be("Income == 5");
15501566

15511567
// Act : string
1552-
var expressionTextUserName = "StaticHelper.Filter(\"UserName == \"\"x\"\"\")";
1568+
// Replace " with \"
1569+
// Replace \" with \\\"
1570+
StaticHelper.Filter("UserName == \"x\"");
1571+
var expressionTextUserName = "StaticHelper.Filter(\"UserName == \\\"x\\\"\")";
15531572
var lambdaUserName = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionTextUserName, user);
15541573
var funcUserName = (Expression<Func<User, string>>)lambdaUserName;
15551574

@@ -1558,33 +1577,28 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio
15581577

15591578
// Assert : string
15601579
resultUserName.Should().Be(@"UserName == ""x""");
1561-
}
15621580

1563-
[Fact]
1564-
public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpression1String()
1565-
{
1566-
// Arrange
1567-
var config = new ParsingConfig
1581+
// Act : string
1582+
// Replace " with \"
1583+
// Replace \" with \"\"
1584+
var configNonDefault = new ParsingConfig
15681585
{
1569-
CustomTypeProvider = new TestCustomTypeProvider()
1586+
CustomTypeProvider = new TestCustomTypeProvider(),
1587+
StringLiteralParsing = StringLiteralParsingType.EscapeDoubleQuoteByTwoDoubleQuotes
15701588
};
1589+
expressionTextUserName = "StaticHelper.Filter(\"UserName == \"\"x\"\"\")";
1590+
lambdaUserName = DynamicExpressionParser.ParseLambda(configNonDefault, typeof(User), null, expressionTextUserName, user);
1591+
funcUserName = (Expression<Func<User, string>>)lambdaUserName;
15711592

1572-
var user = new User();
1593+
delegateUserName = funcUserName.Compile();
1594+
resultUserName = (string?)delegateUserName.DynamicInvoke(user);
15731595

1574-
// Act
1575-
var expressionText = @"StaticHelper.In(Id, StaticHelper.SubSelect(""Identity"", ""LegalPerson"", ""StaticHelper.In(ParentId, StaticHelper.SubSelect(""""LegalPersonId"""", """"PointSiteTD"""", """"Identity = 5"""", """"""""))"", """"))";
1576-
var lambda = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionText, user);
1577-
var func = (Expression<Func<User, bool>>)lambda;
1578-
1579-
var compile = func.Compile();
1580-
var result = (bool?)compile.DynamicInvoke(user);
1581-
1582-
// Assert
1583-
result.Should().Be(false);
1596+
// Assert : string
1597+
resultUserName.Should().Be(@"UserName == ""x""");
15841598
}
15851599

15861600
[Fact]
1587-
public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpression2String()
1601+
public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpressionString()
15881602
{
15891603
// Arrange
15901604
var config = new ParsingConfig
@@ -1594,8 +1608,12 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexEx
15941608

15951609
var user = new User();
15961610

1611+
// Replace " with \"
1612+
// Replace \" with \\\"
1613+
var _ = StaticHelper.In(Guid.NewGuid(), StaticHelper.SubSelect("Identity", "LegalPerson", "StaticHelper.In(ParentId, StaticHelper.SubSelect( \"LegalPersonId\", \"PointSiteTD\", \"Identity = 5\", \"\")) ", ""));
1614+
var expressionText = "StaticHelper.In(Id, StaticHelper.SubSelect(\"Identity\", \"LegalPerson\", \"StaticHelper.In(ParentId, StaticHelper.SubSelect(\\\"LegalPersonId\\\", \\\"PointSiteTD\\\", \\\"Identity = 5\\\", \\\"\\\"))\", \"\"))";
1615+
15971616
// Act
1598-
var expressionText = @"StaticHelper.In(Id, StaticHelper.SubSelect(""Identity"", ""LegalPerson"", ""StaticHelper.In(ParentId, StaticHelper.SubSelect(""""LegalPersonId"""", """"PointSiteTD"""", """"Identity = "" + StaticHelper.ToExpressionString(StaticHelper.Get(""CurrentPlace""), 2) + """""", """"""""))"", """"))";
15991617
var lambda = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionText, user);
16001618
var func = (Expression<Func<User, bool>>)lambda;
16011619

test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs

+40-13
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ public class StringParserTests
1010
[Theory]
1111
[InlineData("'s")]
1212
[InlineData("\"s")]
13-
public void StringParser_With_UnexpectedUnclosedString_ThrowsException(string input)
13+
public void StringParser_ParseStringAndUnescape_With_UnexpectedUnclosedString_ThrowsException(string input)
1414
{
1515
// Act
16-
var exception = Assert.Throws<ParseException>(() => StringParser.ParseString(input));
16+
var exception = Assert.Throws<ParseException>(() => StringParser.ParseStringAndUnescape(input));
1717

1818
// Assert
1919
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
2323
[InlineData("")]
2424
[InlineData(null)]
2525
[InlineData("x")]
26-
public void StringParser_With_InvalidStringLength_ThrowsException(string input)
26+
public void StringParser_ParseStringAndUnescape_With_InvalidStringLength_ThrowsException(string input)
2727
{
2828
// Act
29-
Action action = () => StringParser.ParseString(input);
29+
Action action = () => StringParser.ParseStringAndUnescape(input);
3030

3131
// Assert
3232
action.Should().Throw<ParseException>().WithMessage($"String '{input}' should have at least 2 characters.");
@@ -35,41 +35,41 @@ public void StringParser_With_InvalidStringLength_ThrowsException(string input)
3535
[Theory]
3636
[InlineData("xx")]
3737
[InlineData(" ")]
38-
public void StringParser_With_InvalidStringQuoteCharacter_ThrowsException(string input)
38+
public void StringParser_ParseStringAndUnescape_With_InvalidStringQuoteCharacter_ThrowsException(string input)
3939
{
4040
// Act
41-
Action action = () => StringParser.ParseString(input);
41+
Action action = () => StringParser.ParseStringAndUnescape(input);
4242

4343
// Assert
4444
action.Should().Throw<ParseException>().WithMessage("An escaped string should start with a double (\") or a single (') quote.");
4545
}
4646

4747
[Fact]
48-
public void StringParser_With_UnexpectedUnrecognizedEscapeSequence_ThrowsException()
48+
public void StringParser_ParseStringAndUnescape_With_UnexpectedUnrecognizedEscapeSequence_ThrowsException()
4949
{
5050
// Arrange
5151
var input = new string(new[] { '"', '\\', 'u', '?', '"' });
5252

5353
// Act
54-
Action action = () => StringParser.ParseString(input);
54+
Action action = () => StringParser.ParseStringAndUnescape(input);
5555

5656
// Assert
5757
var parseException = action.Should().Throw<ParseException>();
5858

5959
parseException.Which.InnerException!.Message.Should().Contain("hexadecimal digits");
6060

61-
parseException.Which.StackTrace.Should().Contain("at System.Linq.Dynamic.Core.Parser.StringParser.ParseString(String s, Int32 pos) in ").And.Contain("StringParser.cs:line ");
61+
parseException.Which.StackTrace.Should().Contain("at System.Linq.Dynamic.Core.Parser.StringParser.ParseStringAndUnescape(String s, Int32 pos) in ").And.Contain("StringParser.cs:line ");
6262
}
6363

6464
[Theory]
6565
[InlineData("''", "")]
6666
[InlineData("'s'", "s")]
6767
[InlineData("'\\\\'", "\\")]
6868
[InlineData("'\\n'", "\n")]
69-
public void StringParser_Parse_SingleQuotedString(string input, string expectedResult)
69+
public void StringParser_ParseStringAndUnescape_SingleQuotedString(string input, string expectedResult)
7070
{
7171
// Act
72-
var result = StringParser.ParseString(input);
72+
var result = StringParser.ParseStringAndUnescape(input);
7373

7474
// Assert
7575
result.Should().Be(expectedResult);
@@ -93,12 +93,39 @@ public void StringParser_Parse_SingleQuotedString(string input, string expectedR
9393
[InlineData("\"\\\"\\\"\"", "\"\"")]
9494
[InlineData("\"AB YZ 19 \uD800\udc05 \u00e4\"", "AB YZ 19 \uD800\udc05 \u00e4")]
9595
[InlineData("\"\\\\\\\\192.168.1.1\\\\audio\\\\new\"", "\\\\192.168.1.1\\audio\\new")]
96-
public void StringParser_Parse_DoubleQuotedString(string input, string expectedResult)
96+
[InlineData("\"{\\\"PropertyA\\\":\\\"\\\"}\"", @"{""PropertyA"":""""}")] // #786
97+
public void StringParser_ParseStringAndUnescape_DoubleQuotedString(string input, string expectedResult)
9798
{
9899
// Act
99-
var result = StringParser.ParseString(input);
100+
var result = StringParser.ParseStringAndUnescape(input);
100101

101102
// Assert
102103
result.Should().Be(expectedResult);
103104
}
105+
106+
[Fact]
107+
public void StringParser_ParseStringAndUnescape()
108+
{
109+
// Arrange
110+
var test = "\"x\\\"X\"";
111+
112+
// Act
113+
var result = StringParser.ParseStringAndUnescape(test);
114+
115+
// Assert
116+
result.Should().Be("x\"X");
117+
}
118+
119+
[Fact]
120+
public void StringParser_ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote()
121+
{
122+
// Arrange
123+
var test = "\"x\"\"X\"";
124+
125+
// Act
126+
var result = StringParser.ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(test);
127+
128+
// Assert
129+
result.Should().Be("x\"X");
130+
}
104131
}

test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public static StaticHelperSqlExpression SubSelect(string columnName, string obje
3737
CustomTypeProvider = new TestCustomTypeProvider()
3838
};
3939

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

4343
return new StaticHelperSqlExpression

0 commit comments

Comments
 (0)