diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 7b192cfed..edbe23d96 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -11,6 +11,10 @@ using System.Security; using System.Text; +#if CoreCLR +using System.Runtime.InteropServices; +#endif + namespace Microsoft.PowerShell.EditorServices { /// @@ -355,15 +359,15 @@ private void RecursivelyFindReferences( } } - private string ResolveFilePath(string filePath) + internal string ResolveFilePath(string filePath) { if (!IsPathInMemory(filePath)) { if (filePath.StartsWith(@"file://")) { + filePath = Workspace.UnescapeDriveColon(filePath); // Client sent the path in URI format, extract the local path - Uri fileUri = new Uri(Uri.UnescapeDataString(filePath)); - filePath = fileUri.LocalPath; + filePath = new Uri(filePath).LocalPath; } // Clients could specify paths with escaped space, [ and ] characters which .NET APIs @@ -486,6 +490,40 @@ private string ResolveRelativeScriptPath(string baseFilePath, string relativePat return combinedPath; } + /// + /// Takes a file-scheme URI with an escaped colon after the drive letter and unescapes only the colon. + /// VSCode sends escaped colons after drive letters, but System.Uri expects unescaped. + /// + /// The fully-escaped file-scheme URI string. + /// A file-scheme URI string with the drive colon unescaped. + private static string UnescapeDriveColon(string fileUri) + { +#if CoreCLR + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return fileUri; + } +#endif + // Check here that we have something like "file:///C%3A/" as a prefix (caller must check the file:// part) + if (!(fileUri[7] == '/' && + char.IsLetter(fileUri[8]) && + fileUri[9] == '%' && + fileUri[10] == '3' && + fileUri[11] == 'A' && + fileUri[12] == '/')) + { + return fileUri; + } + + var sb = new StringBuilder(fileUri.Length - 2); // We lost "%3A" and gained ":", so length - 2 + sb.Append("file:///"); + sb.Append(fileUri[8]); // The drive letter + sb.Append(':'); + sb.Append(fileUri.Substring(12)); // The rest of the URI after the colon + + return sb.ToString(); + } + #endregion } } diff --git a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs index d40e59511..b93d796d0 100644 --- a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs @@ -70,5 +70,24 @@ public void CanDetermineIsPathInMemory() $"Testing path {testCase.Path}"); } } + + [Theory()] + [InlineData("file:///C%3A/banana/", @"C:\banana\")] + [InlineData("file:///C%3A/banana/ex.ps1", @"C:\banana\ex.ps1")] + [InlineData("file:///E%3A/Path/to/awful%23path", @"E:\Path\to\awful#path")] + [InlineData("file:///path/with/no/drive", @"C:\path\with\no\drive")] + [InlineData("file:///path/wi[th]/squ[are/brackets/", @"C:\path\wi[th]\squ[are\brackets\")] + [InlineData("file:///Carrots/A%5Ere/Good/", @"C:\Carrots\A^re\Good\")] + [InlineData("file:///Users/barnaby/%E8%84%9A%E6%9C%AC/Reduce-Directory", @"C:\Users\barnaby\脚本\Reduce-Directory")] + [InlineData("file:///C%3A/Program%20Files%20%28x86%29/PowerShell/6/pwsh.exe", @"C:\Program Files (x86)\PowerShell\6\pwsh.exe")] + [InlineData("file:///home/maxim/test%20folder/%D0%9F%D0%B0%D0%BF%D0%BA%D0%B0/helloworld.ps1", @"C:\home\maxim\test folder\Папка\helloworld.ps1")] + public void CorrectlyResolvesPaths(string givenPath, string expectedPath) + { + Workspace workspace = new Workspace(PowerShellVersion, Logging.NullLogger); + + string resolvedPath = workspace.ResolveFilePath(givenPath); + + Assert.Equal(expectedPath, resolvedPath); + } } }