Skip to content

Commit e71b356

Browse files
authored
Fix StringParser to handle (escaped) newline correctly (#546)
* Fix StringParser * Regex.Unescape * TargetFrameworks
1 parent c977fd8 commit e71b356

File tree

6 files changed

+78
-63
lines changed

6 files changed

+78
-63
lines changed

src-console/ConsoleAppEF2.1.1/Program.cs

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Linq.Dynamic.Core;
55
using System.Linq.Dynamic.Core.CustomTypeProviders;
6+
using System.Linq.Expressions;
67
using System.Reflection;
78
using System.Runtime.CompilerServices;
89
using ConsoleAppEF2.Database;
@@ -74,6 +75,13 @@ private static IQueryable GetQueryable()
7475

7576
static void Main(string[] args)
7677
{
78+
var value = "\"\\\\192.168.1.1\\audio\\new\"";
79+
var dataParameter = Expression.Parameter(typeof(Dictionary<string, object>), "data");
80+
var sourceExpr = DynamicExpressionParser.ParseLambda(new[] { dataParameter }, typeof(object), value);
81+
var func = sourceExpr.Compile();
82+
var resultX = func.DynamicInvoke(new Dictionary<string, object>());
83+
Console.WriteLine(resultX);
84+
7785
IQueryable qry = GetQueryable();
7886

7987
var result = qry.Select("it").OrderBy("Value");
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Globalization;
22
using System.Linq.Dynamic.Core.Exceptions;
3-
using System.Text;
43
using System.Text.RegularExpressions;
54

65
namespace System.Linq.Dynamic.Core.Parser
@@ -13,69 +12,30 @@ internal static class StringParser
1312
{
1413
public static string ParseString(string s)
1514
{
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)
15+
if (s == null || s.Length < 2)
2416
{
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++;
17+
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.InvalidStringLength, s, 2), 0);
5418
}
5519

56-
if (found == null)
20+
if (s[0] != '"' && s[0] != '\'')
5721
{
58-
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.UnexpectedUnclosedString, pos, inputStringBuilder.ToString()), pos);
22+
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.InvalidStringQuoteCharacter), 0);
5923
}
6024

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-
;
25+
char quote = s[0]; // This can be single or a double quote
26+
if (s.Last() != quote)
27+
{
28+
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.UnexpectedUnclosedString, s.Length, s), s.Length);
29+
}
7730

78-
return sb.ToString();
31+
try
32+
{
33+
return Regex.Unescape(s.Substring(1, s.Length - 2));
34+
}
35+
catch (Exception ex)
36+
{
37+
throw new ParseException(ex.Message, 0);
38+
}
7939
}
8040
}
8141
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ internal static class Res
4343
public const string InvalidIntegerLiteral = "Invalid integer literal '{0}'";
4444
public const string InvalidIntegerQualifier = "Invalid integer literal qualifier '{0}'";
4545
public const string InvalidRealLiteral = "Invalid real literal '{0}'";
46+
public const string InvalidStringQuoteCharacter = "An escaped string should start with a double (\") or a single (') quote.";
47+
public const string InvalidStringLength = "String '{0}' should have at least {1} characters.";
4648
public const string IsNullRequiresTwoArgs = "The 'isnull' function requires two arguments";
4749
public const string MethodIsVoid = "Method '{0}' in type '{1}' does not return a value";
4850
public const string MethodsAreInaccessible = "Methods on type '{0}' are not accessible";

src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<Description>This is a .NETStandard / .NET Core port of the the Microsoft assembly for the .Net 4.0 Dynamic language functionality.</Description>
77
<AssemblyTitle>System.Linq.Dynamic.Core</AssemblyTitle>
88
<Authors>ZZZ Projects;Stef Heyenrath;Microsoft;Scott Guthrie;King Wilder;Nathan Arnott</Authors>
9+
<!-- <TargetFrameworks>net35;net40;net45;net46;netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp2.1;net5.0</TargetFrameworks> -->
910
<TargetFrameworks>net35;net40;net45;net46;netstandard1.3;netstandard2.0;netstandard2.1;uap10.0;netcoreapp2.1;net5.0</TargetFrameworks>
1011
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1112
<AssemblyName>System.Linq.Dynamic.Core</AssemblyName>
@@ -51,7 +52,7 @@
5152
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
5253
<NugetTargetMoniker>UAP,Version=v10.0</NugetTargetMoniker>
5354
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
54-
<TargetPlatformVersion>10.0.18362.0</TargetPlatformVersion>
55+
<TargetPlatformVersion>10.0.20348.0</TargetPlatformVersion>
5556
<TargetPlatformMinVersion>10.0.10240.0</TargetPlatformMinVersion>
5657
<TargetFrameworkIdentifier>.NETCore</TargetFrameworkIdentifier>
5758
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>

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

+14
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,20 @@ public void DynamicExpressionParser_ParseLambda_StringLiteralEscapedBackslash_Re
866866
Assert.Equal("\"test\\string\"", rightValue);
867867
}
868868

869+
[Fact]
870+
public void DynamicExpressionParser_ParseLambda_StringLiteralEscapedNewline_ReturnsBooleanLambdaExpression()
871+
{
872+
// Act
873+
var expression = DynamicExpressionParser.ParseLambda(
874+
new[] { Expression.Parameter(typeof(string), "Property1") },
875+
typeof(bool),
876+
string.Format("Property1 == {0}", "\"test\\\\new\""));
877+
878+
string rightValue = ((BinaryExpression)expression.Body).Right.ToString();
879+
Assert.Equal(typeof(Boolean), expression.Body.Type);
880+
Assert.Equal("\"test\\new\"", rightValue);
881+
}
882+
869883
[Fact]
870884
public void DynamicExpressionParser_ParseLambda_StringLiteral_Backslash()
871885
{

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

+35-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class StringParserTests
1010
[Theory]
1111
[InlineData("'s")]
1212
[InlineData("\"s")]
13-
public void StringParser_WithUnexpectedUnclosedString_ThrowsException(string input)
13+
public void StringParser_With_UnexpectedUnclosedString_ThrowsException(string input)
1414
{
1515
// Act
1616
var exception = Assert.Throws<ParseException>(() => StringParser.ParseString(input));
@@ -19,23 +19,49 @@ public void StringParser_WithUnexpectedUnclosedString_ThrowsException(string inp
1919
Assert.Equal($"Unexpected end of string with unclosed string at position 2 near '{input}'.", exception.Message);
2020
}
2121

22+
[Theory]
23+
[InlineData("")]
24+
[InlineData(null)]
25+
[InlineData("x")]
26+
public void StringParser_With_InvalidStringLength_ThrowsException(string input)
27+
{
28+
// Act
29+
Action action = () => StringParser.ParseString(input);
30+
31+
// Assert
32+
action.Should().Throw<ParseException>().WithMessage($"String '{input}' should have at least 2 characters.");
33+
}
34+
35+
[Theory]
36+
[InlineData("xx")]
37+
[InlineData(" ")]
38+
public void StringParser_With_InvalidStringQuoteCharacter_ThrowsException(string input)
39+
{
40+
// Act
41+
Action action = () => StringParser.ParseString(input);
42+
43+
// Assert
44+
action.Should().Throw<ParseException>().WithMessage("An escaped string should start with a double (\") or a single (') quote.");
45+
}
46+
2247
[Fact]
2348
public void StringParser_With_UnexpectedUnrecognizedEscapeSequence_ThrowsException()
2449
{
2550
// Arrange
2651
string input = new string(new[] { '"', '\\', 'u', '?', '"' });
2752

2853
// Act
29-
var exception = Assert.Throws<ParseException>(() => StringParser.ParseString(input));
54+
Action action = () => StringParser.ParseString(input);
3055

3156
// Assert
32-
Assert.Equal("Unexpected unrecognized escape sequence at position 1 near '\\u?'.", exception.Message);
57+
action.Should().Throw<ParseException>();
3358
}
3459

3560
[Theory]
61+
[InlineData("''", "")]
3662
[InlineData("'s'", "s")]
37-
[InlineData("'\\r'", "\r")]
3863
[InlineData("'\\\\'", "\\")]
64+
[InlineData("'\\n'", "\n")]
3965
public void StringParser_Parse_SingleQuotedString(string input, string expectedResult)
4066
{
4167
// Act
@@ -47,6 +73,10 @@ public void StringParser_Parse_SingleQuotedString(string input, string expectedR
4773

4874
[Theory]
4975
[InlineData("\"\"", "")]
76+
[InlineData("\"\\\\\"", "\\")]
77+
[InlineData("\"\\n\"", "\n")]
78+
[InlineData("\"\\\\n\"", "\\n")]
79+
[InlineData("\"\\\\new\"", "\\new")]
5080
[InlineData("\"[]\"", "[]")]
5181
[InlineData("\"()\"", "()")]
5282
[InlineData("\"(\\\"\\\")\"", "(\"\")")]
@@ -57,8 +87,8 @@ public void StringParser_Parse_SingleQuotedString(string input, string expectedR
5787
[InlineData("\"ab\\\"cd\"", "ab\"cd")]
5888
[InlineData("\"\\\"\"", "\"")]
5989
[InlineData("\"\\\"\\\"\"", "\"\"")]
60-
[InlineData("\"\\\\\"", "\\")]
6190
[InlineData("\"AB YZ 19 \uD800\udc05 \u00e4\"", "AB YZ 19 \uD800\udc05 \u00e4")]
91+
[InlineData("\"\\\\\\\\192.168.1.1\\\\audio\\\\new\"", "\\\\192.168.1.1\\audio\\new")]
6292
public void StringParser_Parse_DoubleQuotedString(string input, string expectedResult)
6393
{
6494
// Act

0 commit comments

Comments
 (0)