Skip to content

Commit 2bf6b66

Browse files
committed
PowershellArgumentNeedsEscaping does not account for a single quoted string
Fixes #1608 Apply suggested Rosyln Fix
1 parent 37f53f5 commit 2bf6b66

File tree

4 files changed

+87
-15
lines changed

4 files changed

+87
-15
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -163,16 +163,9 @@ private static PSCommand BuildPSCommandFromArguments(string command, IReadOnlyLi
163163

164164
foreach (string arg in arguments)
165165
{
166-
sb.Append(' ');
167-
168-
if (StringEscaping.PowerShellArgumentNeedsEscaping(arg))
169-
{
170-
sb.Append(StringEscaping.SingleQuoteAndEscape(arg));
171-
}
172-
else
173-
{
174-
sb.Append(arg);
175-
}
166+
sb
167+
.Append(' ')
168+
.Append(StringEscaping.EscapePowershellArgument(arg));
176169
}
177170

178171
return new PSCommand().AddScript(sb.ToString());

src/PowerShellEditorServices/Utility/StringEscaping.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,24 @@ internal static class StringEscaping
88
{
99
public static StringBuilder SingleQuoteAndEscape(string s)
1010
{
11+
var dequotedString = s.TrimStart('\'').TrimEnd('\'');
12+
var psEscapedInnerQuotes = dequotedString.Replace("'", "`'");
1113
return new StringBuilder(s.Length)
12-
.Append("'")
13-
.Append(s.Replace("'", "''"))
14-
.Append("'");
14+
.Append('\'')
15+
.Append(psEscapedInnerQuotes)
16+
.Append('\'');
1517
}
1618

1719
public static bool PowerShellArgumentNeedsEscaping(string argument)
1820
{
21+
//Already quoted arguments dont require escaping unless there is a quote inside as well
22+
if (argument.StartsWith("'") && argument.EndsWith("'"))
23+
{
24+
var dequotedString = argument.TrimStart('\'').TrimEnd('\'');
25+
// need to escape if there is a single quote between single quotes
26+
return dequotedString.Contains("'");
27+
}
28+
1929
foreach (char c in argument)
2030
{
2131
switch (c)
@@ -33,5 +43,18 @@ public static bool PowerShellArgumentNeedsEscaping(string argument)
3343

3444
return false;
3545
}
46+
47+
public static string EscapePowershellArgument(string argument)
48+
{
49+
if (PowerShellArgumentNeedsEscaping(argument))
50+
{
51+
return SingleQuoteAndEscape(argument).ToString();
52+
}
53+
else
54+
{
55+
return argument;
56+
}
57+
}
58+
3659
}
3760
}

test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,13 @@ public void CorrectlyWildcardEscapesPaths_Spaces(string unescapedPath, string es
6565
[InlineData("C:\\look\\an*\\here.ps1", "'C:\\look\\an*\\here.ps1'")]
6666
[InlineData("/Users/me/Documents/?here.ps1", "'/Users/me/Documents/?here.ps1'")]
6767
[InlineData("/Brackets [and s]paces/path.ps1", "'/Brackets [and s]paces/path.ps1'")]
68-
[InlineData("/file path/that isn't/normal/", "'/file path/that isn''t/normal/'")]
68+
[InlineData("/file path/that isn't/normal/", "'/file path/that isn`'t/normal/'")]
6969
[InlineData("/CJK.chars/脚本/hello.ps1", "'/CJK.chars/脚本/hello.ps1'")]
7070
[InlineData("/CJK chars/脚本/[hello].ps1", "'/CJK chars/脚本/[hello].ps1'")]
7171
[InlineData("C:\\Animal s\\утка\\quack.ps1", "'C:\\Animal s\\утка\\quack.ps1'")]
7272
[InlineData("C:\\&nimals\\утка\\qu*ck?.ps1", "'C:\\&nimals\\утка\\qu*ck?.ps1'")]
73+
[InlineData("../../Quote'InPathTest.ps1", "'../../Quote`'InPathTest.ps1'")]
74+
7375
public void CorrectlyQuoteEscapesPaths(string unquotedPath, string expectedQuotedPath)
7476
{
7577
string extensionQuotedPath = StringEscaping.SingleQuoteAndEscape(unquotedPath).ToString();
@@ -87,7 +89,7 @@ public void CorrectlyQuoteEscapesPaths(string unquotedPath, string expectedQuote
8789
[InlineData("C:\\look\\an*\\here.ps1", "'C:\\look\\an`*\\here.ps1'")]
8890
[InlineData("/Users/me/Documents/?here.ps1", "'/Users/me/Documents/`?here.ps1'")]
8991
[InlineData("/Brackets [and s]paces/path.ps1", "'/Brackets `[and s`]paces/path.ps1'")]
90-
[InlineData("/file path/that isn't/normal/", "'/file path/that isn''t/normal/'")]
92+
[InlineData("/file path/that isn't/normal/", "'/file path/that isn`'t/normal/'")]
9193
[InlineData("/CJK.chars/脚本/hello.ps1", "'/CJK.chars/脚本/hello.ps1'")]
9294
[InlineData("/CJK chars/脚本/[hello].ps1", "'/CJK chars/脚本/`[hello`].ps1'")]
9395
[InlineData("C:\\Animal s\\утка\\quack.ps1", "'C:\\Animal s\\утка\\quack.ps1'")]
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using Xunit;
2+
using Microsoft.PowerShell.EditorServices.Utility;
3+
using System.Management.Automation;
4+
using System.Linq;
5+
6+
namespace Microsoft.PowerShell.EditorServices.Test.Session
7+
{
8+
public class ArgumentEscapingTests
9+
{
10+
[Trait("Category", "ArgumentEscaping")]
11+
[Theory]
12+
[InlineData("/path/to/file", "/path/to/file")]
13+
[InlineData("'/path/to/file'", "'/path/to/file'")]
14+
[InlineData("not|allowed|pipeline", "'not|allowed|pipeline'")]
15+
[InlineData("doublequote\"inmiddle", "'doublequote\"inmiddle'")]
16+
[InlineData("am&persand", "'am&persand'")]
17+
[InlineData("semicolon;", "'semicolon;'")]
18+
[InlineData(":colon", "':colon'")]
19+
[InlineData(" has space s", "' has space s'")]
20+
[InlineData("[brackets]areOK", "[brackets]areOK")]
21+
[InlineData("$(expressionsAreOK)", "$(expressionsAreOK)")]
22+
[InlineData("{scriptBlocksAreOK}", "{scriptBlocksAreOK}")]
23+
[InlineData("'quote ' in middle of argument'", "'quote `' in middle of argument'")]
24+
25+
public void CorrectlyEscapesPowerShellArguments(string Arg, string expectedArg)
26+
{
27+
string quotedArg = StringEscaping.EscapePowershellArgument(Arg);
28+
Assert.Equal(expectedArg, quotedArg);
29+
}
30+
31+
[Trait("Category", "ArgumentEscaping")]
32+
[Theory]
33+
[InlineData("/path/to/file", "/path/to/file")]
34+
[InlineData("'/path/to/file'", "/path/to/file")]
35+
[InlineData("not|allowed|pipeline", "not|allowed|pipeline")]
36+
[InlineData("doublequote\"inmiddle", "doublequote\"inmiddle")]
37+
[InlineData("am&persand", "am&persand")]
38+
[InlineData("semicolon;", "semicolon;")]
39+
[InlineData(":colon", ":colon")]
40+
[InlineData(" has space s", " has space s")]
41+
[InlineData("[brackets]areOK", "[brackets]areOK")]
42+
[InlineData("$(echo 'expressionsAreOK')", "expressionsAreOK")]
43+
// [InlineData("{scriptBlocksAreOK}", "{scriptBlocksAreOK}")]
44+
public void CanEvaluateArgumentsSafely(string Arg, string expectedOutput)
45+
{
46+
var escapedArg = StringEscaping.EscapePowershellArgument(Arg);
47+
var psCommand = new PSCommand().AddScript($"& Write-Output {escapedArg}");
48+
using var pwsh = System.Management.Automation.PowerShell.Create();
49+
pwsh.Commands = psCommand;
50+
var scriptOutput = pwsh.Invoke<string>().First();
51+
Assert.Equal(expectedOutput, scriptOutput);
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)