Skip to content

Conversation

blaadje
Copy link

@blaadje blaadje commented Aug 24, 2025

Description

This change adds proper normalization of URIs between WSL and Windows paths in PathMapping.
It fixes the issue where using --path-mappings=/mnt/c/=C:/ with clangd.exe from WSL produced hybrid paths mixing WSL and Windows fragments.

What’s been done:

  • In clang-tools-extra/clangd/PathMapping.cpp :

    • Normalize URIs containing encoded Windows fragments (C:%5c...).

    • Convert /mnt/<drive>/... into /X:/... when a drive letter is detected.

    • Adjust mapping logic to prevent hybrid paths.

  • In clang-tools-extra/clangd/unittests/PathMappingTests.cpp :

    • Added test for WSL → Windows conversion (ClientToServer).
    • Added test for Windows → WSL conversion (ServerToClient).

Resources :

@blaadje blaadje marked this pull request as draft August 24, 2025 20:04
Copy link

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@blaadje blaadje force-pushed the feature/pathmapping-normalize-wsl branch 3 times, most recently from c25724d to 14bd578 Compare August 24, 2025 20:38
@blaadje blaadje force-pushed the feature/pathmapping-normalize-wsl branch from 14bd578 to f527c9c Compare August 24, 2025 20:46
@blaadje blaadje changed the title Feature/pathmapping normalize wsl clangd(PathMapping): normalize mixed WSL/Windows URIs Aug 24, 2025
@blaadje blaadje marked this pull request as ready for review August 24, 2025 21:53
@llvmbot
Copy link
Member

llvmbot commented Aug 24, 2025

@llvm/pr-subscribers-clangd

Author: Alexandre (blaadje)

Changes

Description

This change adds proper normalization of URIs between WSL and Windows paths in PathMapping.
It fixes the issue where using --path-mappings=/mnt/c/=C:/ with clangd.exe from WSL produced hybrid paths mixing WSL and Windows fragments.

What’s been done:

  • In clang-tools-extra/clangd/PathMapping.cpp :

    • Normalize URIs containing encoded Windows fragments (C:%5c...).

    • Convert /mnt/&lt;drive&gt;/... into /X:/... when a drive letter is detected.

    • Adjust mapping logic to prevent hybrid paths.

  • In clang-tools-extra/clangd/unittests/PathMappingTests.cpp :

    • Added test for WSL → Windows conversion (ClientToServer).
    • Added test for Windows → WSL conversion (ServerToClient).

Resources :


Full diff: https://github.com/llvm/llvm-project/pull/155191.diff

2 Files Affected:

  • (modified) clang-tools-extra/clangd/PathMapping.cpp (+70-5)
  • (modified) clang-tools-extra/clangd/unittests/PathMappingTests.cpp (+22)
diff --git a/clang-tools-extra/clangd/PathMapping.cpp b/clang-tools-extra/clangd/PathMapping.cpp
index 4b93ff2c60c5c..68cdf0e3f037e 100644
--- a/clang-tools-extra/clangd/PathMapping.cpp
+++ b/clang-tools-extra/clangd/PathMapping.cpp
@@ -12,6 +12,7 @@
 #include "llvm/Support/Error.h"
 #include "llvm/Support/Path.h"
 #include <algorithm>
+#include <cctype>
 #include <optional>
 #include <tuple>
 
@@ -28,6 +29,30 @@ std::optional<std::string> doPathMapping(llvm::StringRef S,
     llvm::consumeError(Uri.takeError());
     return std::nullopt;
   }
+
+  std::string BodyStr = (*Uri).body().str();
+  std::replace(BodyStr.begin(), BodyStr.end(), '\\', '/');
+
+  bool DidPreferEmbeddedDrive = false;
+  if (BodyStr.rfind("/mnt/", 0) == 0 && BodyStr.size() > 6) {
+    for (size_t i = 0; i + 2 < BodyStr.size(); ++i) {
+      if (std::isalpha((unsigned char)BodyStr[i]) && BodyStr[i + 1] == ':' &&
+          BodyStr[i + 2] == '/') {
+        BodyStr = std::string("/") + BodyStr.substr(i);
+        DidPreferEmbeddedDrive = true;
+        break;
+      }
+    }
+  }
+
+  if (Dir == PathMapping::Direction::ClientToServer && DidPreferEmbeddedDrive) {
+    if (BodyStr.size() >= 3 && BodyStr[0] == '/' &&
+        std::isalpha((unsigned char)BodyStr[1]) && BodyStr[2] == ':') {
+      return URI((*Uri).scheme(), (*Uri).authority(), BodyStr).toString();
+    }
+  }
+
+  llvm::StringRef BodyRef(BodyStr);
   for (const auto &Mapping : Mappings) {
     const std::string &From = Dir == PathMapping::Direction::ClientToServer
                                   ? Mapping.ClientPath
@@ -35,13 +60,53 @@ std::optional<std::string> doPathMapping(llvm::StringRef S,
     const std::string &To = Dir == PathMapping::Direction::ClientToServer
                                 ? Mapping.ServerPath
                                 : Mapping.ClientPath;
-    llvm::StringRef Body = Uri->body();
-    if (Body.consume_front(From) && (Body.empty() || Body.front() == '/')) {
-      std::string MappedBody = (To + Body).str();
-      return URI(Uri->scheme(), Uri->authority(), MappedBody)
-          .toString();
+    llvm::StringRef Working = BodyRef;
+    if (Working.consume_front(From)) {
+      if (From.empty() || From.back() == '/' || Working.empty() ||
+          Working.front() == '/') {
+        llvm::StringRef Adjusted = Working;
+
+        char MappingDrive = 0;
+        if (To.size() >= 3 && To[0] == '/' &&
+            std::isalpha((unsigned char)To[1]) && To[2] == ':')
+          MappingDrive = To[1];
+        if (MappingDrive) {
+          for (size_t i = 0; i + 2 < (size_t)Working.size(); ++i) {
+            char c = Working[i];
+            if (std::isalpha((unsigned char)c) && Working[i + 1] == ':' &&
+                Working[i + 2] == '/') {
+              if (std::tolower((unsigned char)c) ==
+                  std::tolower((unsigned char)MappingDrive)) {
+                Adjusted = Working.substr(i + 3);
+                break;
+              }
+            }
+          }
+        }
+        std::string MappedBody = (To + Adjusted).str();
+        return URI((*Uri).scheme(), (*Uri).authority(), MappedBody).toString();
+      }
+    }
+  }
+
+  if (Dir == PathMapping::Direction::ServerToClient) {
+    for (const auto &Mapping : Mappings) {
+      const std::string &From = Mapping.ServerPath;
+      const std::string &To = Mapping.ClientPath;
+      if (From.empty())
+        continue;
+      llvm::StringRef W = BodyRef;
+      if (W.starts_with(From)) {
+        llvm::StringRef Rest = W.substr(From.size());
+        if (Rest.empty() || Rest.front() == '/') {
+          std::string MappedBody = (To + Rest).str();
+          return URI((*Uri).scheme(), (*Uri).authority(), MappedBody)
+              .toString();
+        }
+      }
     }
   }
+
   return std::nullopt;
 }
 
diff --git a/clang-tools-extra/clangd/unittests/PathMappingTests.cpp b/clang-tools-extra/clangd/unittests/PathMappingTests.cpp
index 2e26148495a01..a5bc54e7c1d72 100644
--- a/clang-tools-extra/clangd/unittests/PathMappingTests.cpp
+++ b/clang-tools-extra/clangd/unittests/PathMappingTests.cpp
@@ -212,6 +212,28 @@ TEST(ApplyPathMappingTests, MapsKeys) {
   EXPECT_EQ(*Params, *ExpectedParams);
 }
 
+
+TEST(DoPathMappingTests, HandlesEncodedWindowsFragmentsInbound) {
+  // Simulate a client sending a WSL-style path that embeds an encoded Windows
+  // fragment (percent-encoded backslashes and drive letter). The server should
+  // normalize and map it to a Windows-style path.
+  PathMappings Mappings{{"/mnt/c/", "/C:/"}};
+  const char *Orig = "file:///mnt/c/projects/cod2-asi/C:%5cUsers%5caukx%5cprojects%5ccod2-asi%5csrc%5cedge_detection.h";
+  std::optional<std::string> Mapped = doPathMapping(Orig, PathMapping::Direction::ClientToServer, Mappings);
+  ASSERT_TRUE(bool(Mapped));
+  EXPECT_EQ(*Mapped, std::string("file:///C:/Users/aukx/projects/cod2-asi/src/edge_detection.h"));
+}
+
+TEST(DoPathMappingTests, HandlesWindowsToWslOutbound) {
+  // Server returns Windows-style URIs; they should be mapped back to the
+  // client's WSL-style paths before sending out.
+  PathMappings Mappings{{"/mnt/c/", "/C:/"}};
+  const char *Orig = "file:///C:/projects/cod2-asi/src/edge_detection.h";
+  std::optional<std::string> Mapped = doPathMapping(Orig, PathMapping::Direction::ServerToClient, Mappings);
+  ASSERT_TRUE(bool(Mapped));
+  EXPECT_EQ(*Mapped, std::string("file:///mnt/c/projects/cod2-asi/src/edge_detection.h"));
+}
+
 } // namespace
 } // namespace clangd
 } // namespace clang

@llvmbot
Copy link
Member

llvmbot commented Aug 24, 2025

@llvm/pr-subscribers-clang-tools-extra

Author: Alexandre (blaadje)

Changes

Description

This change adds proper normalization of URIs between WSL and Windows paths in PathMapping.
It fixes the issue where using --path-mappings=/mnt/c/=C:/ with clangd.exe from WSL produced hybrid paths mixing WSL and Windows fragments.

What’s been done:

  • In clang-tools-extra/clangd/PathMapping.cpp :

    • Normalize URIs containing encoded Windows fragments (C:%5c...).

    • Convert /mnt/&lt;drive&gt;/... into /X:/... when a drive letter is detected.

    • Adjust mapping logic to prevent hybrid paths.

  • In clang-tools-extra/clangd/unittests/PathMappingTests.cpp :

    • Added test for WSL → Windows conversion (ClientToServer).
    • Added test for Windows → WSL conversion (ServerToClient).

Resources :


Full diff: https://github.com/llvm/llvm-project/pull/155191.diff

2 Files Affected:

  • (modified) clang-tools-extra/clangd/PathMapping.cpp (+70-5)
  • (modified) clang-tools-extra/clangd/unittests/PathMappingTests.cpp (+22)
diff --git a/clang-tools-extra/clangd/PathMapping.cpp b/clang-tools-extra/clangd/PathMapping.cpp
index 4b93ff2c60c5c..68cdf0e3f037e 100644
--- a/clang-tools-extra/clangd/PathMapping.cpp
+++ b/clang-tools-extra/clangd/PathMapping.cpp
@@ -12,6 +12,7 @@
 #include "llvm/Support/Error.h"
 #include "llvm/Support/Path.h"
 #include <algorithm>
+#include <cctype>
 #include <optional>
 #include <tuple>
 
@@ -28,6 +29,30 @@ std::optional<std::string> doPathMapping(llvm::StringRef S,
     llvm::consumeError(Uri.takeError());
     return std::nullopt;
   }
+
+  std::string BodyStr = (*Uri).body().str();
+  std::replace(BodyStr.begin(), BodyStr.end(), '\\', '/');
+
+  bool DidPreferEmbeddedDrive = false;
+  if (BodyStr.rfind("/mnt/", 0) == 0 && BodyStr.size() > 6) {
+    for (size_t i = 0; i + 2 < BodyStr.size(); ++i) {
+      if (std::isalpha((unsigned char)BodyStr[i]) && BodyStr[i + 1] == ':' &&
+          BodyStr[i + 2] == '/') {
+        BodyStr = std::string("/") + BodyStr.substr(i);
+        DidPreferEmbeddedDrive = true;
+        break;
+      }
+    }
+  }
+
+  if (Dir == PathMapping::Direction::ClientToServer && DidPreferEmbeddedDrive) {
+    if (BodyStr.size() >= 3 && BodyStr[0] == '/' &&
+        std::isalpha((unsigned char)BodyStr[1]) && BodyStr[2] == ':') {
+      return URI((*Uri).scheme(), (*Uri).authority(), BodyStr).toString();
+    }
+  }
+
+  llvm::StringRef BodyRef(BodyStr);
   for (const auto &Mapping : Mappings) {
     const std::string &From = Dir == PathMapping::Direction::ClientToServer
                                   ? Mapping.ClientPath
@@ -35,13 +60,53 @@ std::optional<std::string> doPathMapping(llvm::StringRef S,
     const std::string &To = Dir == PathMapping::Direction::ClientToServer
                                 ? Mapping.ServerPath
                                 : Mapping.ClientPath;
-    llvm::StringRef Body = Uri->body();
-    if (Body.consume_front(From) && (Body.empty() || Body.front() == '/')) {
-      std::string MappedBody = (To + Body).str();
-      return URI(Uri->scheme(), Uri->authority(), MappedBody)
-          .toString();
+    llvm::StringRef Working = BodyRef;
+    if (Working.consume_front(From)) {
+      if (From.empty() || From.back() == '/' || Working.empty() ||
+          Working.front() == '/') {
+        llvm::StringRef Adjusted = Working;
+
+        char MappingDrive = 0;
+        if (To.size() >= 3 && To[0] == '/' &&
+            std::isalpha((unsigned char)To[1]) && To[2] == ':')
+          MappingDrive = To[1];
+        if (MappingDrive) {
+          for (size_t i = 0; i + 2 < (size_t)Working.size(); ++i) {
+            char c = Working[i];
+            if (std::isalpha((unsigned char)c) && Working[i + 1] == ':' &&
+                Working[i + 2] == '/') {
+              if (std::tolower((unsigned char)c) ==
+                  std::tolower((unsigned char)MappingDrive)) {
+                Adjusted = Working.substr(i + 3);
+                break;
+              }
+            }
+          }
+        }
+        std::string MappedBody = (To + Adjusted).str();
+        return URI((*Uri).scheme(), (*Uri).authority(), MappedBody).toString();
+      }
+    }
+  }
+
+  if (Dir == PathMapping::Direction::ServerToClient) {
+    for (const auto &Mapping : Mappings) {
+      const std::string &From = Mapping.ServerPath;
+      const std::string &To = Mapping.ClientPath;
+      if (From.empty())
+        continue;
+      llvm::StringRef W = BodyRef;
+      if (W.starts_with(From)) {
+        llvm::StringRef Rest = W.substr(From.size());
+        if (Rest.empty() || Rest.front() == '/') {
+          std::string MappedBody = (To + Rest).str();
+          return URI((*Uri).scheme(), (*Uri).authority(), MappedBody)
+              .toString();
+        }
+      }
     }
   }
+
   return std::nullopt;
 }
 
diff --git a/clang-tools-extra/clangd/unittests/PathMappingTests.cpp b/clang-tools-extra/clangd/unittests/PathMappingTests.cpp
index 2e26148495a01..a5bc54e7c1d72 100644
--- a/clang-tools-extra/clangd/unittests/PathMappingTests.cpp
+++ b/clang-tools-extra/clangd/unittests/PathMappingTests.cpp
@@ -212,6 +212,28 @@ TEST(ApplyPathMappingTests, MapsKeys) {
   EXPECT_EQ(*Params, *ExpectedParams);
 }
 
+
+TEST(DoPathMappingTests, HandlesEncodedWindowsFragmentsInbound) {
+  // Simulate a client sending a WSL-style path that embeds an encoded Windows
+  // fragment (percent-encoded backslashes and drive letter). The server should
+  // normalize and map it to a Windows-style path.
+  PathMappings Mappings{{"/mnt/c/", "/C:/"}};
+  const char *Orig = "file:///mnt/c/projects/cod2-asi/C:%5cUsers%5caukx%5cprojects%5ccod2-asi%5csrc%5cedge_detection.h";
+  std::optional<std::string> Mapped = doPathMapping(Orig, PathMapping::Direction::ClientToServer, Mappings);
+  ASSERT_TRUE(bool(Mapped));
+  EXPECT_EQ(*Mapped, std::string("file:///C:/Users/aukx/projects/cod2-asi/src/edge_detection.h"));
+}
+
+TEST(DoPathMappingTests, HandlesWindowsToWslOutbound) {
+  // Server returns Windows-style URIs; they should be mapped back to the
+  // client's WSL-style paths before sending out.
+  PathMappings Mappings{{"/mnt/c/", "/C:/"}};
+  const char *Orig = "file:///C:/projects/cod2-asi/src/edge_detection.h";
+  std::optional<std::string> Mapped = doPathMapping(Orig, PathMapping::Direction::ServerToClient, Mappings);
+  ASSERT_TRUE(bool(Mapped));
+  EXPECT_EQ(*Mapped, std::string("file:///mnt/c/projects/cod2-asi/src/edge_detection.h"));
+}
+
 } // namespace
 } // namespace clangd
 } // namespace clang

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants