diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index 7750585c0..1ded392ff 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -163,16 +163,9 @@ private static PSCommand BuildPSCommandFromArguments(string command, IReadOnlyLi foreach (string arg in arguments) { - sb.Append(' '); - - if (StringEscaping.PowerShellArgumentNeedsEscaping(arg)) - { - sb.Append(StringEscaping.SingleQuoteAndEscape(arg)); - } - else - { - sb.Append(arg); - } + sb + .Append(' ') + .Append(StringEscaping.EscapePowershellArgument(arg)); } return new PSCommand().AddScript(sb.ToString()); diff --git a/src/PowerShellEditorServices/Utility/StringEscaping.cs b/src/PowerShellEditorServices/Utility/StringEscaping.cs index 5736a9aad..2113d20d4 100644 --- a/src/PowerShellEditorServices/Utility/StringEscaping.cs +++ b/src/PowerShellEditorServices/Utility/StringEscaping.cs @@ -8,14 +8,24 @@ internal static class StringEscaping { public static StringBuilder SingleQuoteAndEscape(string s) { + var dequotedString = s.TrimStart('\'').TrimEnd('\''); + var psEscapedInnerQuotes = dequotedString.Replace("'", "`'"); return new StringBuilder(s.Length) - .Append("'") - .Append(s.Replace("'", "''")) - .Append("'"); + .Append('\'') + .Append(psEscapedInnerQuotes) + .Append('\''); } public static bool PowerShellArgumentNeedsEscaping(string argument) { + //Already quoted arguments dont require escaping unless there is a quote inside as well + if (argument.StartsWith("'") && argument.EndsWith("'")) + { + var dequotedString = argument.TrimStart('\'').TrimEnd('\''); + // need to escape if there is a single quote between single quotes + return dequotedString.Contains("'"); + } + foreach (char c in argument) { switch (c) @@ -33,5 +43,18 @@ public static bool PowerShellArgumentNeedsEscaping(string argument) return false; } + + public static string EscapePowershellArgument(string argument) + { + if (PowerShellArgumentNeedsEscaping(argument)) + { + return SingleQuoteAndEscape(argument).ToString(); + } + else + { + return argument; + } + } + } } diff --git a/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs b/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs index 495166e09..34c8a4951 100644 --- a/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs @@ -65,11 +65,13 @@ public void CorrectlyWildcardEscapesPaths_Spaces(string unescapedPath, string es [InlineData("C:\\look\\an*\\here.ps1", "'C:\\look\\an*\\here.ps1'")] [InlineData("/Users/me/Documents/?here.ps1", "'/Users/me/Documents/?here.ps1'")] [InlineData("/Brackets [and s]paces/path.ps1", "'/Brackets [and s]paces/path.ps1'")] - [InlineData("/file path/that isn't/normal/", "'/file path/that isn''t/normal/'")] + [InlineData("/file path/that isn't/normal/", "'/file path/that isn`'t/normal/'")] [InlineData("/CJK.chars/脚本/hello.ps1", "'/CJK.chars/脚本/hello.ps1'")] [InlineData("/CJK chars/脚本/[hello].ps1", "'/CJK chars/脚本/[hello].ps1'")] [InlineData("C:\\Animal s\\утка\\quack.ps1", "'C:\\Animal s\\утка\\quack.ps1'")] [InlineData("C:\\&nimals\\утка\\qu*ck?.ps1", "'C:\\&nimals\\утка\\qu*ck?.ps1'")] + [InlineData("../../Quote'InPathTest.ps1", "'../../Quote`'InPathTest.ps1'")] + public void CorrectlyQuoteEscapesPaths(string unquotedPath, string expectedQuotedPath) { string extensionQuotedPath = StringEscaping.SingleQuoteAndEscape(unquotedPath).ToString(); diff --git a/test/PowerShellEditorServices.Test/Utility/ArgumentEscapingTests.cs b/test/PowerShellEditorServices.Test/Utility/ArgumentEscapingTests.cs new file mode 100644 index 000000000..002d3819f --- /dev/null +++ b/test/PowerShellEditorServices.Test/Utility/ArgumentEscapingTests.cs @@ -0,0 +1,54 @@ +using Xunit; +using Microsoft.PowerShell.EditorServices.Utility; +using System.Management.Automation; +using System.Linq; + +namespace Microsoft.PowerShell.EditorServices.Test.Session +{ + public class ArgumentEscapingTests + { + [Trait("Category", "ArgumentEscaping")] + [Theory] + [InlineData("/path/to/file", "/path/to/file")] + [InlineData("'/path/to/file'", "'/path/to/file'")] + [InlineData("not|allowed|pipeline", "'not|allowed|pipeline'")] + [InlineData("doublequote\"inmiddle", "'doublequote\"inmiddle'")] + [InlineData("am&persand", "'am&persand'")] + [InlineData("semicolon;", "'semicolon;'")] + [InlineData(":colon", "':colon'")] + [InlineData(" has space s", "' has space s'")] + [InlineData("[brackets]areOK", "[brackets]areOK")] + [InlineData("$(expressionsAreOK)", "$(expressionsAreOK)")] + [InlineData("{scriptBlocksAreOK}", "{scriptBlocksAreOK}")] + [InlineData("'quote ' in middle of argument'", "'quote `' in middle of argument'")] + + public void CorrectlyEscapesPowerShellArguments(string Arg, string expectedArg) + { + string quotedArg = StringEscaping.EscapePowershellArgument(Arg); + Assert.Equal(expectedArg, quotedArg); + } + + [Trait("Category", "ArgumentEscaping")] + [Theory] + [InlineData("/path/to/file", "/path/to/file")] + [InlineData("'/path/to/file'", "/path/to/file")] + [InlineData("not|allowed|pipeline", "not|allowed|pipeline")] + [InlineData("doublequote\"inmiddle", "doublequote\"inmiddle")] + [InlineData("am&persand", "am&persand")] + [InlineData("semicolon;", "semicolon;")] + [InlineData(":colon", ":colon")] + [InlineData(" has space s", " has space s")] + [InlineData("[brackets]areOK", "[brackets]areOK")] + [InlineData("$(echo 'expressionsAreOK')", "expressionsAreOK")] + // [InlineData("{scriptBlocksAreOK}", "{scriptBlocksAreOK}")] + public void CanEvaluateArgumentsSafely(string Arg, string expectedOutput) + { + var escapedArg = StringEscaping.EscapePowershellArgument(Arg); + var psCommand = new PSCommand().AddScript($"& Write-Output {escapedArg}"); + using var pwsh = System.Management.Automation.PowerShell.Create(); + pwsh.Commands = psCommand; + var scriptOutput = pwsh.Invoke().First(); + Assert.Equal(expectedOutput, scriptOutput); + } + } +}