Skip to content

Commit f11a606

Browse files
authored
Fixes for parsing escaped / quoted strings (#326)
* StringParser
1 parent f41c708 commit f11a606

File tree

7 files changed

+209
-52
lines changed

7 files changed

+209
-52
lines changed

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

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections;
1+
using JetBrains.Annotations;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.ComponentModel;
45
using System.Globalization;
@@ -10,7 +11,6 @@
1011
using System.Linq.Dynamic.Core.Validation;
1112
using System.Linq.Expressions;
1213
using System.Reflection;
13-
using JetBrains.Annotations;
1414

1515
namespace System.Linq.Dynamic.Core.Parser
1616
{
@@ -781,37 +781,23 @@ Expression ParsePrimaryStart()
781781
Expression ParseStringLiteral()
782782
{
783783
_textParser.ValidateToken(TokenId.StringLiteral);
784-
char quote = _textParser.CurrentToken.Text[0];
785-
string s = _textParser.CurrentToken.Text.Substring(1, _textParser.CurrentToken.Text.Length - 2);
786-
int index1 = 0;
787-
while (true)
788-
{
789-
int index2 = s.IndexOf(quote, index1);
790-
if (index2 < 0)
791-
{
792-
break;
793-
}
794784

795-
if (index2 + 1 < s.Length && s[index2 + 1] == quote)
796-
{
797-
s = s.Remove(index2, 1);
798-
}
799-
index1 = index2 + 1;
800-
}
785+
string result = StringParser.ParseString(_textParser.CurrentToken.Text);
801786

802-
if (quote == '\'')
787+
if (_textParser.CurrentToken.Text[0] == '\'')
803788
{
804-
if (s.Length != 1)
789+
if (result.Length > 1)
805790
{
806791
throw ParseError(Res.InvalidCharacterLiteral);
807792
}
793+
808794
_textParser.NextToken();
809-
return ConstantExpressionHelper.CreateLiteral(s[0], s);
795+
return ConstantExpressionHelper.CreateLiteral(result[0], result);
810796
}
797+
811798
_textParser.NextToken();
812-
return ConstantExpressionHelper.CreateLiteral(s, s);
799+
return ConstantExpressionHelper.CreateLiteral(result, result);
813800
}
814-
815801
Expression ParseIntegerLiteral()
816802
{
817803
_textParser.ValidateToken(TokenId.IntegerLiteral);
@@ -1522,7 +1508,7 @@ Expression ParseTypeAccess(Type type)
15221508

15231509
// If only 1 argument, and the arg is ConstantExpression, return the conversion
15241510
// If only 1 argument, and the arg is null, return the conversion (Can't use constructor)
1525-
if (args.Length == 1
1511+
if (args.Length == 1
15261512
&& (args[0] == null || args[0] is ConstantExpression))
15271513
{
15281514
return GenerateConversion(args[0], type, errorPos);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System.Globalization;
2+
using System.Linq.Dynamic.Core.Exceptions;
3+
using System.Text;
4+
using System.Text.RegularExpressions;
5+
6+
namespace System.Linq.Dynamic.Core.Parser
7+
{
8+
/// <summary>
9+
/// Parse a Double and Single Quoted string.
10+
/// Some parts of the code is based on https://github.com/zzzprojects/Eval-Expression.NET
11+
/// </summary>
12+
internal static class StringParser
13+
{
14+
public static string ParseString(string s)
15+
{
16+
var inputStringBuilder = new StringBuilder(s);
17+
var tempStringBuilder = new StringBuilder();
18+
string found = null;
19+
20+
char quote = inputStringBuilder[0];
21+
int pos = 1;
22+
23+
while (pos < inputStringBuilder.Length)
24+
{
25+
char ch = inputStringBuilder[pos];
26+
27+
if (ch == '\\' && pos + 1 < inputStringBuilder.Length && (inputStringBuilder[pos + 1] == '\\' || inputStringBuilder[pos + 1] == quote))
28+
{
29+
tempStringBuilder.Append(inputStringBuilder[pos + 1]);
30+
pos++; // Treat as escape character for \\ or \'
31+
}
32+
else if (ch == '\\' && pos + 1 < inputStringBuilder.Length && inputStringBuilder[pos + 1] == 'u')
33+
{
34+
if (pos + 5 >= inputStringBuilder.Length)
35+
{
36+
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.UnexpectedUnrecognizedEscapeSequence, pos, inputStringBuilder.ToString(pos, inputStringBuilder.Length - pos - 1)), pos);
37+
}
38+
39+
string unicode = inputStringBuilder.ToString(pos, 6);
40+
tempStringBuilder.Append(Regex.Unescape(unicode));
41+
pos += 5;
42+
}
43+
else if (ch == quote)
44+
{
45+
found = Replace(tempStringBuilder);
46+
break;
47+
}
48+
else
49+
{
50+
tempStringBuilder.Append(ch);
51+
}
52+
53+
pos++;
54+
}
55+
56+
if (found == null)
57+
{
58+
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.UnexpectedUnclosedString, pos, inputStringBuilder.ToString()), pos);
59+
}
60+
61+
return found;
62+
}
63+
64+
private static string Replace(StringBuilder inputStringBuilder)
65+
{
66+
var sb = new StringBuilder(inputStringBuilder.ToString())
67+
.Replace(@"\\", "\\") // \\ – backslash
68+
.Replace(@"\0", "\0") // Unicode character 0
69+
.Replace(@"\a", "\a") // Alert(character 7)
70+
.Replace(@"\b", "\b") // Backspace(character 8)
71+
.Replace(@"\f", "\f") // Form feed(character 12)
72+
.Replace(@"\n", "\n") // New line(character 10)
73+
.Replace(@"\r", "\r") // Carriage return (character 13)
74+
.Replace(@"\t", "\t") // Horizontal tab(character 9)
75+
.Replace(@"\v", "\v") // Vertical quote(character 11)
76+
;
77+
78+
return sb.ToString();
79+
}
80+
}
81+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ internal static class Res
6262
public const string TokenExpected = "{0} expected";
6363
public const string TypeHasNoNullableForm = "Type '{0}' has no nullable form";
6464
public const string TypeNotFound = "Type '{0}' not found";
65+
public const string UnexpectedUnclosedString = "Unexpected end of string with unclosed string at position {0} near '{1}'.";
66+
public const string UnexpectedUnrecognizedEscapeSequence = "Unexpected unrecognized escape sequence at position {0} near '{1}'.";
6567
public const string UnknownIdentifier = "Unknown identifier '{0}'";
6668
public const string UnknownPropertyOrField = "No property or field '{0}' exists in type '{1}'";
6769
public const string UnterminatedStringLiteral = "Unterminated string literal";

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

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -609,17 +609,15 @@ public void DynamicExpressionParser_ParseLambda_Config_StringLiteralEmpty_Return
609609
[Fact]
610610
public void DynamicExpressionParser_ParseLambda_StringLiteralEmbeddedQuote_ReturnsBooleanLambdaExpression()
611611
{
612-
string expectedRightValue = "\"test \\\"string\"";
613-
614612
// Act
615613
var expression = DynamicExpressionParser.ParseLambda(
616614
new[] { Expression.Parameter(typeof(string), "Property1") },
617615
typeof(bool),
618-
string.Format("Property1 == {0}", expectedRightValue));
616+
string.Format("Property1 == {0}", "\"test \\\"string\""));
619617

620618
string rightValue = ((BinaryExpression)expression.Body).Right.ToString();
621619
Assert.Equal(typeof(bool), expression.Body.Type);
622-
Assert.Equal(expectedRightValue, rightValue);
620+
Assert.Equal("\"test \"string\"", rightValue);
623621
}
624622

625623
/// <summary>
@@ -659,17 +657,15 @@ public void DynamicExpressionParser_ParseLambda_MultipleLambdas()
659657
[Fact]
660658
public void DynamicExpressionParser_ParseLambda_StringLiteralStartEmbeddedQuote_ReturnsBooleanLambdaExpression()
661659
{
662-
// Assign
663-
string expectedRightValue = "\"\\\"test\"";
664-
660+
// Act
665661
var expression = DynamicExpressionParser.ParseLambda(
666662
new[] { Expression.Parameter(typeof(string), "Property1") },
667663
typeof(bool),
668-
string.Format("Property1 == {0}", expectedRightValue));
664+
string.Format("Property1 == {0}", "\"\\\"test\""));
669665

670666
string rightValue = ((BinaryExpression)expression.Body).Right.ToString();
671667
Assert.Equal(typeof(bool), expression.Body.Type);
672-
Assert.Equal(expectedRightValue, rightValue);
668+
Assert.Equal("\"\"test\"", rightValue);
673669
}
674670

675671
[Fact]
@@ -686,51 +682,51 @@ public void DynamicExpressionParser_ParseLambda_StringLiteral_MissingClosingQuot
686682
[Fact]
687683
public void DynamicExpressionParser_ParseLambda_StringLiteralEscapedBackslash_ReturnsBooleanLambdaExpression()
688684
{
689-
// Assign
690-
string expectedRightValue = "\"test\\string\"";
691-
692685
// Act
693686
var expression = DynamicExpressionParser.ParseLambda(
694687
new[] { Expression.Parameter(typeof(string), "Property1") },
695688
typeof(bool),
696-
string.Format("Property1 == {0}", expectedRightValue));
689+
string.Format("Property1 == {0}", "\"test\\\\string\""));
697690

698691
string rightValue = ((BinaryExpression)expression.Body).Right.ToString();
699692
Assert.Equal(typeof(Boolean), expression.Body.Type);
700-
Assert.Equal(expectedRightValue, rightValue);
693+
Assert.Equal("\"test\\string\"", rightValue);
701694
}
702695

703696
[Fact]
704697
public void DynamicExpressionParser_ParseLambda_StringLiteral_Backslash()
705698
{
706-
string expectedLeftValue = "Property1.IndexOf(\"\\\\\")";
699+
// Assign
707700
string expectedRightValue = "0";
701+
702+
//Act
708703
var expression = DynamicExpressionParser.ParseLambda(
709704
new[] { Expression.Parameter(typeof(string), "Property1") },
710705
typeof(Boolean),
711-
string.Format("{0} >= {1}", expectedLeftValue, expectedRightValue));
706+
string.Format("{0} >= {1}", "Property1.IndexOf(\"\\\\\")", expectedRightValue));
712707

713708
string leftValue = ((BinaryExpression)expression.Body).Left.ToString();
714709
string rightValue = ((BinaryExpression)expression.Body).Right.ToString();
710+
711+
// Assert
715712
Assert.Equal(typeof(Boolean), expression.Body.Type);
716-
Assert.Equal(expectedLeftValue, leftValue);
713+
Assert.Equal("Property1.IndexOf(\"\\\")", leftValue);
717714
Assert.Equal(expectedRightValue, rightValue);
718715
}
719716

720717
[Fact]
721718
public void DynamicExpressionParser_ParseLambda_StringLiteral_QuotationMark()
722719
{
723-
string expectedLeftValue = "Property1.IndexOf(\"\\\"\")";
724720
string expectedRightValue = "0";
725721
var expression = DynamicExpressionParser.ParseLambda(
726722
new[] { Expression.Parameter(typeof(string), "Property1") },
727723
typeof(Boolean),
728-
string.Format("{0} >= {1}", expectedLeftValue, expectedRightValue));
724+
string.Format("{0} >= {1}", "Property1.IndexOf(\"\\\"\")", expectedRightValue));
729725

730726
string leftValue = ((BinaryExpression)expression.Body).Left.ToString();
731727
string rightValue = ((BinaryExpression)expression.Body).Right.ToString();
732728
Assert.Equal(typeof(Boolean), expression.Body.Type);
733-
Assert.Equal(expectedLeftValue, leftValue);
729+
Assert.Equal("Property1.IndexOf(\"\"\")", leftValue);
734730
Assert.Equal(expectedRightValue, rightValue);
735731
}
736732

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1733,6 +1733,24 @@ public void ExpressionTests_StringConcatenation()
17331733
Assert.Equal("FirstNamex y", resultAddWithAmpAndParams.First());
17341734
}
17351735

1736+
[Fact]
1737+
public void ExpressionTests_StringEscaping()
1738+
{
1739+
// Arrange
1740+
var baseQuery = new[] { new { Value = "ab\"cd" }, new { Value = "a \\ b" } }.AsQueryable();
1741+
1742+
// Act
1743+
var result1 = baseQuery.Where("it.Value == \"ab\\\"cd\"").ToList();
1744+
var result2 = baseQuery.Where("it.Value.IndexOf('\\\\') != -1").ToList();
1745+
1746+
// Assert
1747+
Assert.Single(result1);
1748+
Assert.Equal("ab\"cd", result1[0].Value);
1749+
1750+
Assert.Single(result2);
1751+
Assert.Equal("a \\ b", result2[0].Value);
1752+
}
1753+
17361754
[Fact]
17371755
public void ExpressionTests_BinaryAnd()
17381756
{
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System.Linq.Dynamic.Core.Exceptions;
2+
using System.Linq.Dynamic.Core.Parser;
3+
using FluentAssertions;
4+
using Xunit;
5+
6+
namespace System.Linq.Dynamic.Core.Tests.Parser
7+
{
8+
public class StringParserTests
9+
{
10+
[Theory]
11+
[InlineData("'s")]
12+
[InlineData("\"s")]
13+
public void StringParser_WithUnexpectedUnclosedString_ThrowsException(string input)
14+
{
15+
// Act
16+
var exception = Assert.Throws<ParseException>(() => StringParser.ParseString(input));
17+
18+
// Assert
19+
Assert.Equal($"Unexpected end of string with unclosed string at position 2 near '{input}'.", exception.Message);
20+
}
21+
22+
[Fact]
23+
public void StringParser_With_UnexpectedUnrecognizedEscapeSequence_ThrowsException()
24+
{
25+
// Arrange
26+
string input = new string(new[] { '"', '\\', 'u', '?', '"' });
27+
28+
// Act
29+
var exception = Assert.Throws<ParseException>(() => StringParser.ParseString(input));
30+
31+
// Assert
32+
Assert.Equal("Unexpected unrecognized escape sequence at position 1 near '\\u?'.", exception.Message);
33+
}
34+
35+
[Theory]
36+
[InlineData("'s'", "s")]
37+
[InlineData("'\\r'", "\r")]
38+
[InlineData("'\\\\'", "\\")]
39+
public void StringParser_Parse_SingleQuotedString(string input, string expectedResult)
40+
{
41+
// Act
42+
string result = StringParser.ParseString(input);
43+
44+
// Assert
45+
result.Should().Be(expectedResult);
46+
}
47+
48+
[Theory]
49+
[InlineData("\"\"", "")]
50+
[InlineData("\"[]\"", "[]")]
51+
[InlineData("\"()\"", "()")]
52+
[InlineData("\"(\\\"\\\")\"", "(\"\")")]
53+
[InlineData("\"/\"", "/")]
54+
[InlineData("\"a\"", "a")]
55+
[InlineData("\"This \\\"is\\\" a test.\"", "This \"is\" a test.")]
56+
[InlineData(@"""This \""is\"" b test.""", @"This ""is"" b test.")]
57+
[InlineData("\"ab\\\"cd\"", "ab\"cd")]
58+
[InlineData("\"\\\"\"", "\"")]
59+
[InlineData("\"\\\"\\\"\"", "\"\"")]
60+
[InlineData("\"\\\\\"", "\\")]
61+
[InlineData("\"AB YZ 19 \uD800\udc05 \u00e4\"", "AB YZ 19 \uD800\udc05 \u00e4")]
62+
public void StringParser_Parse_DoubleQuotedString(string input, string expectedResult)
63+
{
64+
// Act
65+
string result = StringParser.ParseString(input);
66+
67+
// Assert
68+
result.Should().Be(expectedResult);
69+
}
70+
}
71+
}

test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,22 +124,25 @@ public void Where_Dynamic_Exceptions()
124124
[Fact]
125125
public void Where_Dynamic_StringQuoted()
126126
{
127-
//Arrange
127+
// Arrange
128128
var testList = User.GenerateSampleModels(2, allowNullableProfiles: true);
129129
testList[0].UserName = @"This \""is\"" a test.";
130130
var qry = testList.AsQueryable();
131131

132-
//Act
133-
var result1a = qry.Where(@"UserName == ""This \""is\"" a test.""").ToArray();
134-
var result1b = qry.Where("UserName == \"This \\\"is\\\" a test.\"").ToArray();
135-
var result2 = qry.Where("UserName == @0", @"This \""is\"" a test.").ToArray();
132+
// Act
133+
// var result1a = qry.Where(@"UserName == ""This \\""is\\"" a test.""").ToArray();
134+
var result1b = qry.Where("UserName == \"This \\\\\\\"is\\\\\\\" a test.\"").ToArray();
135+
var result2a = qry.Where("UserName == @0", @"This \""is\"" a test.").ToArray();
136+
var result2b = qry.Where("UserName == @0", "This \\\"is\\\" a test.").ToArray();
137+
136138
var expected = qry.Where(x => x.UserName == @"This \""is\"" a test.").ToArray();
137139

138-
//Assert
140+
// Assert
139141
Assert.Single(expected);
140-
Assert.Equal(expected, result1a);
142+
// Assert.Equal(expected, result1a);
141143
Assert.Equal(expected, result1b);
142-
Assert.Equal(expected, result2);
144+
Assert.Equal(expected, result2a);
145+
Assert.Equal(expected, result2b);
143146
}
144147

145148
[Fact]

0 commit comments

Comments
 (0)