fix: Fix Windows MakeRelativePath.

On Windows, it is invalid to make a relative path span across different drives. This fix:
1. Instead of prepending slash, we remove slash for both paths if there's one.
2. Return input's absolute path if the drive letter is different between input and dest.

Fixes: https://bugs.chromium.org/p/gn/issues/detail?id=317
Change-Id: I3ee2a46d412f663b0739f95fe3683d892b8fe6b5
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/16620
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Reviewed-by: David Turner <digit@google.com>
Commit-Queue: David Turner <digit@google.com>
diff --git a/src/gn/filesystem_utils.cc b/src/gn/filesystem_utils.cc
index d37750b..d747289 100644
--- a/src/gn/filesystem_utils.cc
+++ b/src/gn/filesystem_utils.cc
@@ -17,6 +17,7 @@
 #include "util/build_config.h"
 
 #if defined(OS_WIN)
+#include <direct.h>
 #include <windows.h>
 #endif
 
@@ -639,33 +640,75 @@
 #endif
 }
 
-std::string MakeRelativePath(const std::string& input,
-                             const std::string& dest) {
 #if defined(OS_WIN)
-  // Make sure that absolute |input| path starts with a slash if |dest| path
-  // does. Otherwise skipping common prefixes won't work properly. Ensure the
-  // same for |dest| path too.
-  if (IsPathAbsolute(input) && !IsSlash(input[0]) && IsSlash(dest[0])) {
-    std::string corrected_input(1, dest[0]);
-    corrected_input.append(input);
-    return MakeRelativePath(corrected_input, dest);
-  }
-  if (IsPathAbsolute(dest) && !IsSlash(dest[0]) && IsSlash(input[0])) {
-    std::string corrected_dest(1, input[0]);
-    corrected_dest.append(dest);
-    return MakeRelativePath(input, corrected_dest);
+std::string GetPathWithDriveLetter(std::string_view path) {
+  if (!IsPathAbsolute(path) || !IsSlash(path[0]))
+    return std::string(path);
+
+  int drive = _getdrive();
+  DCHECK(drive > 0 && drive <= 26);
+
+  std::string ret;
+  ret.reserve(2 + path.size());
+  ret.push_back('A' + drive - 1);
+  ret.push_back(':');
+  ret += path;
+  return ret;
+}
+
+// Regulate path if it is an absolute path.
+std::string RegulatePathIfAbsolute(std::string_view path) {
+  CHECK(!path.empty());
+  bool is_start_slash = IsSlash(path[0]);
+
+  // 1. /C:/ -> C:/
+  if (path.size() > 3 && is_start_slash &&
+        base::IsAsciiAlpha(path[1]) && path[2] == ':') {
+    return RegulatePathIfAbsolute(path.substr(1));
   }
 
-  // Make sure that both absolute paths use the same drive letter case.
+  bool is_path_absolute = IsPathAbsolute(path);
+
+  // 2. /Path -> ($PWD's Drive):/Path
+  if (is_path_absolute && is_start_slash) {
+    return GetPathWithDriveLetter(path);
+  }
+
+  // 3. c:/ -> C:/
+  std::string ret(path);
+  if (is_path_absolute && !is_start_slash) {
+    ret[0] = base::ToUpperASCII(path[0]);
+  }
+
+  return ret;
+}
+#endif
+
+std::string MakeRelativePath(std::string_view input,
+                             std::string_view dest) {
+#if defined(OS_WIN)
+  // Regulate the paths.
+  std::string input_regulated = RegulatePathIfAbsolute(input);
+  std::string dest_regulated = RegulatePathIfAbsolute(dest);
+
+  input = input_regulated;
+  dest = dest_regulated;
+
+  // On Windows, it is invalid to make a relative path across different
+  // drive letters. A relative path cannot span over different drives.
+  // For example:
+  //    Input          : D:/Path/Any/Where
+  //    Dest           : C:/Path/In/Another/Drive
+  //    Invalid Result : ../../../../../D:/Path/Any/Where
+  //    Correct Result : D:/Path/Any/Where
+  // It will at least make ninja fail.
+  // See: https://bugs.chromium.org/p/gn/issues/detail?id=317
   if (IsPathAbsolute(input) && IsPathAbsolute(dest) && input.size() > 1 &&
       dest.size() > 1) {
-    int letter_pos = base::IsAsciiAlpha(input[0]) ? 0 : 1;
-    if (input[letter_pos] != dest[letter_pos] &&
-        base::ToUpperASCII(input[letter_pos]) ==
-            base::ToUpperASCII(dest[letter_pos])) {
-      std::string corrected_input = input;
-      corrected_input[letter_pos] = dest[letter_pos];
-      return MakeRelativePath(corrected_input, dest);
+    if (input[0] != dest[0]) {
+      // If the drive letters are differnet, we have no choice but use
+      // the absolute path of input for correctness.
+      return input_regulated;
     }
   }
 #endif
diff --git a/src/gn/filesystem_utils_unittest.cc b/src/gn/filesystem_utils_unittest.cc
index ef25324..6f5baa3 100644
--- a/src/gn/filesystem_utils_unittest.cc
+++ b/src/gn/filesystem_utils_unittest.cc
@@ -543,6 +543,24 @@
   EXPECT_EQ("../../../../path/to/foo",
             RebasePath("/c:/path/to/foo", SourceDir("//a/b"),
                        std::string_view("C:/source/root")));
+
+  // Should be absolute path of destination if the drive letters are
+  // different between two paths.
+  EXPECT_EQ("C:/path/to/foo",
+            RebasePath("C:/path/to/foo", SourceDir("//a/b"),
+                       std::string_view("D:/source/root")));
+  EXPECT_EQ("D:/path/to/foo",
+            RebasePath("D:/path/to/foo", SourceDir("//a/b"),
+                       std::string_view("C:/source/root")));
+  EXPECT_EQ("E:/path/to/foo",
+            RebasePath("/E:/path/to/foo", SourceDir("//a/b"),
+                       std::string_view("/c:/source/root")));
+  EXPECT_EQ("E:/path/to/foo",
+            RebasePath("/e:/path/to/foo", SourceDir("//a/b"),
+                       std::string_view("c:/source/root")));
+  EXPECT_EQ("C:/path/to/foo",
+            RebasePath("/c:/path/to/foo", SourceDir("//a/b"),
+                       std::string_view("D:/source/root")));
 #endif
 }