Skip to content

Commit

Permalink
Merge branch 'master' into stef-789-json
Browse files Browse the repository at this point in the history
  • Loading branch information
StefH committed Jul 1, 2024
2 parents 12946cc + c0f44ef commit ddf6712
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 47 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# v1.4.3 (01 July 2024)
- [#788](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/788) - Fix parsing " [bug] contributed by [StefH](https://github.com/StefH)
- [#805](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/805) - Update ComparisonOperator logic to support comparing to object [feature] contributed by [StefH](https://github.com/StefH)
- [#451](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/451) - Cannot compare 'Object' type [feature]
- [#786](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/786) - Parser interpreting the two consecutive escape sequences \"\" as a single escape sequence [bug]

# v1.4.2 (25 June 2024)
- [#824](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/824) - Fixed: Incorrect Handling of Qualifiers in ConstantExpressionHelper contributed by [RenanCarlosPereira](https://github.com/RenanCarlosPereira)
- [#821](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/821) - Incorrect Handling of Qualifiers in ConstantExpressionHelper [bug]
Expand Down
2 changes: 1 addition & 1 deletion Generate-ReleaseNotes.bat
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
rem https://github.com/StefH/GitHubReleaseNotes

SET version=v1.4.2
SET version=v1.4.3

GitHubReleaseNotes --output CHANGELOG.md --exclude-labels invalid question documentation wontfix environment --language en --version %version% --token %GH_TOKEN%
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
2 changes: 1 addition & 1 deletion version.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<PatchVersion>2</PatchVersion>
<PatchVersion>3</PatchVersion>
</PropertyGroup>
</Project>

0 comments on commit ddf6712

Please sign in to comment.