Windows: implement getCorrectCasing

Implement getCorrectCasing: a function that
normalizes a path and returns the case-correct
version of it.

See https://github.com/bazelbuild/bazel/issues/8799

Closes #9435.

PiperOrigin-RevId: 271083623
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
index 77b78fd..2ad60e2 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
@@ -226,4 +226,9 @@
     return Files.readAttributes(
         file.toPath(), DosFileAttributes.class, symlinkOpts(followSymlinks));
   }
+
+  @VisibleForTesting
+  Path getCorrectCasingForTesting(Path p) throws IOException {
+    return getPath(WindowsFileOperations.getCorrectCasing(p.getPathString()));
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsFileOperations.java b/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsFileOperations.java
index ba82a4f..2fa8e40 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsFileOperations.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/jni/WindowsFileOperations.java
@@ -85,6 +85,8 @@
 
   private static native int nativeDeletePath(String path, String[] error);
 
+  private static native void nativeGetCorrectCasing(String path, String[] result);
+
   /** Determines whether `path` is a junction point or directory symlink. */
   public static boolean isSymlinkOrJunction(String path) throws IOException {
     WindowsJniLoader.loadJni();
@@ -228,4 +230,33 @@
         throw new IOException(String.format("Cannot delete path '%s': %s", path, error[0]));
     }
   }
+
+  /**
+   * Returns the case-corrected copy of <code>absPath</code>
+   *
+   * <p>Windows paths are case-insensitive, but the filesystem preserves the casing. For example you
+   * can create <code>C:\Foo\Bar</code> and access it as <code>c:\FOO\bar</code>, but the correct
+   * casing would be <code>C:\Foo\Bar</code>. Sometimes Bazel needs the correct casing (see
+   * https://github.com/bazelbuild/bazel/issues/8799 for example).
+   *
+   * <p>This method fixes paths to have the correct casing, up to the last existing segment of the
+   * path.
+   *
+   * @param absPath an absolute Windows path. May not be normalized (i.e. have <code>./</code> and
+   *     <code>../</code> segments) and may have <code>/</code> separators instead of <code>\\
+   *     </code>.
+   * @return an absolute, normalized Windows path. The path is case-corrected up to the last
+   *     existing segment of <code>absPath</code>. The rest of the segments are left unchanged (with
+   *     regard to casing) but are normalized and copied into the result.
+   * @throws IOException if <code>absPath</code> was empty or was not absolute
+   */
+  public static String getCorrectCasing(String absPath) throws IOException {
+    WindowsJniLoader.loadJni();
+    String[] result = new String[] {null};
+    nativeGetCorrectCasing(absPath, result);
+    if (result[0].isEmpty()) {
+      throw new IOException(String.format("Path is not absolute: '%s'", absPath));
+    }
+    return removeUncPrefixAndUseSlashes(result[0]);
+  }
 }
diff --git a/src/main/native/windows/file-jni.cc b/src/main/native/windows/file-jni.cc
index 63d81d8..47f5f82 100644
--- a/src/main/native/windows/file-jni.cc
+++ b/src/main/native/windows/file-jni.cc
@@ -141,3 +141,14 @@
   }
   return result;
 }
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_google_devtools_build_lib_windows_jni_WindowsFileOperations_nativeGetCorrectCasing(
+    JNIEnv* env, jclass clazz, jstring path, jobjectArray result_holder) {
+  std::wstring wpath(bazel::windows::GetJavaWstring(env, path));
+  std::wstring result(bazel::windows::GetCorrectCasing(wpath));
+  env->SetObjectArrayElement(
+      result_holder, 0,
+      env->NewString(reinterpret_cast<const jchar*>(result.c_str()),
+                     result.size()));
+}
diff --git a/src/main/native/windows/file.cc b/src/main/native/windows/file.cc
index cc79ead..fa65374 100644
--- a/src/main/native/windows/file.cc
+++ b/src/main/native/windows/file.cc
@@ -781,5 +781,61 @@
   }
 }
 
+std::wstring GetCorrectCasing(const std::wstring& abs_path) {
+  if (!HasDriveSpecifierPrefix(abs_path.c_str())) {
+    return L"";
+  }
+  std::wstring path = Normalize(abs_path);
+  std::unique_ptr<wchar_t[]> result(new wchar_t[4 + path.size() + 1]);
+  // Ensure path starts with UNC prefix, so we can use long paths in
+  // FindFirstFileW.
+  wcscpy(result.get(), L"\\\\?\\");
+  // Copy the rest of the normalized path. (Must be normalized and use `\`
+  // separators, for the UNC prefix to work.)
+  wcscpy(result.get() + 4, path.c_str());
+  // Ensure drive letter is upper case.
+  result[4] = towupper(result[4]);
+  // Start at index 7, which is after the UNC prefix and drive segment (i.e.
+  // past `\\?\C:\`).
+  wchar_t* start = result.get() + 7;
+  // Fix the casing of each segment, from left to right.
+  while (true) {
+    // Find current segment end.
+    wchar_t* seg_end = wcschr(start, L'\\');
+    // Pretend the whole path ends at the current segment.
+    if (seg_end) {
+      *seg_end = 0;
+    }
+    // Find this path from the filesystem. The lookup is case-insensitive, but
+    // the result shows the correct casing. Because we fix the casing from left
+    // to right, only the last segment needs fixing, so we look up that
+    // particular directory (or file).
+    WIN32_FIND_DATAW metadata;
+    HANDLE handle = FindFirstFileW(result.get(), &metadata);
+    if (handle != INVALID_HANDLE_VALUE) {
+      // Found the child. The correct casing is in metadata.cFileName
+      wcscpy(start, metadata.cFileName);
+      FindClose(handle);
+      if (seg_end) {
+        // There are more path segments to fix. Restore the `\` separator.
+        *seg_end = L'\\';
+        start = seg_end + 1;
+      } else {
+        // This was the last segment.
+        break;
+      }
+    } else {
+      // Path does not exist. Restore the `\` separator and leave the rest of
+      // the path unchanged.
+      if (seg_end) {
+        *seg_end = L'\\';
+      }
+      break;
+    }
+  }
+  // Return the case-corrected path without the `\\?\` prefix.
+  return result.get() + 4;
+}
+
 }  // namespace windows
 }  // namespace bazel
diff --git a/src/main/native/windows/file.h b/src/main/native/windows/file.h
index f8651e6..9786687 100644
--- a/src/main/native/windows/file.h
+++ b/src/main/native/windows/file.h
@@ -195,6 +195,23 @@
 
 bool GetCwd(std::wstring* result, DWORD* err_code);
 
+// Normalizes and corrects the casing of 'abs_path'.
+//
+// 'abs_path' must be absolute, and start with a Windows drive prefix.
+// It may have an UNC prefix, may not be normalized, may use '\' and '/' as
+// directory separators.
+//
+// The result is normalized, uses '\' separators, has no trailing '\', and is
+// case-corrected up to the last existing segment. Non-existent tail segments
+// are kept in their casing, but normalized.
+//
+// For example if C:\Foo\Bar exists, then
+// GetCorrectCasing("\\\\?\\c:/FOO/./bar\\") returns "C:\Foo\Bar" and
+// GetCorrectCasing("c:/foo/qux//../bar/BAZ/quX") returns "C:\Foo\Bar\BAZ\quX".
+//
+// If 'abs_path' is null or empty or not absolute, the result is empty.
+std::wstring GetCorrectCasing(const std::wstring& abs_path);
+
 }  // namespace windows
 }  // namespace bazel
 
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
index 3de77fe..1669fa5 100644
--- a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
@@ -45,15 +45,15 @@
 @TestSpec(localOnly = true, supportedOs = OS.WINDOWS)
 public class WindowsFileSystemTest {
 
-  private String scratchRoot;
-  private WindowsTestUtil testUtil;
   private WindowsFileSystem fs;
+  private Path scratchRoot;
+  private WindowsTestUtil testUtil;
 
   @Before
   public void loadJni() throws Exception {
-    scratchRoot = new File(System.getenv("TEST_TMPDIR"), "x").getAbsolutePath();
-    testUtil = new WindowsTestUtil(scratchRoot);
     fs = new WindowsFileSystem(DigestHashFunction.getDefaultUnchecked());
+    scratchRoot = fs.getPath(System.getenv("TEST_TMPDIR")).getRelative("x");
+    testUtil = new WindowsTestUtil(scratchRoot.getPathString());
     cleanupScratchDir();
   }
 
@@ -238,12 +238,12 @@
     String shortPath = "shortp~1.res/foo/withsp~1/bar/~witht~1/hello.txt";
     String longPath = "shortpath.resolution/foo/with spaces/bar/~with tilde/hello.txt";
     testUtil.scratchFile(longPath, "hello");
-    Path p = fs.getPath(scratchRoot).getRelative(shortPath);
+    Path p = scratchRoot.getRelative(shortPath);
     assertThat(p.getPathString()).endsWith(longPath);
-    assertThat(p).isEqualTo(fs.getPath(scratchRoot).getRelative(shortPath));
-    assertThat(p).isEqualTo(fs.getPath(scratchRoot).getRelative(longPath));
-    assertThat(fs.getPath(scratchRoot).getRelative(shortPath)).isEqualTo(p);
-    assertThat(fs.getPath(scratchRoot).getRelative(longPath)).isEqualTo(p);
+    assertThat(p).isEqualTo(scratchRoot.getRelative(shortPath));
+    assertThat(p).isEqualTo(scratchRoot.getRelative(longPath));
+    assertThat(scratchRoot.getRelative(shortPath)).isEqualTo(p);
+    assertThat(scratchRoot.getRelative(longPath)).isEqualTo(p);
   }
 
   @Test
@@ -251,11 +251,11 @@
     String shortPath = "unreso~1.sho/foo/will~1.exi/bar/hello.txt";
     String longPath = "unresolvable.shortpath/foo/will.exist/bar/hello.txt";
     // Assert that we can create an unresolvable path.
-    Path p = fs.getPath(scratchRoot).getRelative(shortPath);
+    Path p = scratchRoot.getRelative(shortPath);
     assertThat(p.getPathString()).endsWith(shortPath);
     // Assert that we can then create the whole path, and can now resolve the short form.
     testUtil.scratchFile(longPath, "hello");
-    Path q = fs.getPath(scratchRoot).getRelative(shortPath);
+    Path q = scratchRoot.getRelative(shortPath);
     assertThat(q.getPathString()).endsWith(longPath);
     assertThat(p).isNotEqualTo(q);
   }
@@ -269,15 +269,15 @@
    */
   @Test
   public void testShortPathResolvesToDifferentPathsOverTime() throws Exception {
-    Path p1 = fs.getPath(scratchRoot).getRelative("longpa~1");
-    Path p2 = fs.getPath(scratchRoot).getRelative("longpa~1");
+    Path p1 = scratchRoot.getRelative("longpa~1");
+    Path p2 = scratchRoot.getRelative("longpa~1");
     assertThat(p1.exists()).isFalse();
     assertThat(p1).isEqualTo(p2);
 
     testUtil.scratchDir("longpathnow");
-    Path q1 = fs.getPath(scratchRoot).getRelative("longpa~1");
+    Path q1 = scratchRoot.getRelative("longpa~1");
     assertThat(q1.exists()).isTrue();
-    assertThat(q1).isEqualTo(fs.getPath(scratchRoot).getRelative("longpathnow"));
+    assertThat(q1).isEqualTo(scratchRoot.getRelative("longpathnow"));
 
     // Delete the original resolution of "longpa~1" ("longpathnow").
     assertThat(q1.delete()).isTrue();
@@ -285,30 +285,30 @@
 
     // Create a directory whose 8dot3 name is also "longpa~1" but its long name is different.
     testUtil.scratchDir("longpaththen");
-    Path r1 = fs.getPath(scratchRoot).getRelative("longpa~1");
+    Path r1 = scratchRoot.getRelative("longpa~1");
     assertThat(r1.exists()).isTrue();
-    assertThat(r1).isEqualTo(fs.getPath(scratchRoot).getRelative("longpaththen"));
+    assertThat(r1).isEqualTo(scratchRoot.getRelative("longpaththen"));
   }
 
   @Test
   public void testCreateSymbolicLink() throws Exception {
     // Create the `scratchRoot` directory.
-    assertThat(fs.getPath(scratchRoot).createDirectory()).isTrue();
+    assertThat(scratchRoot.createDirectory()).isTrue();
     // Create symlink with directory target, relative path.
-    Path link1 = fs.getPath(scratchRoot).getRelative("link1");
+    Path link1 = scratchRoot.getRelative("link1");
     fs.createSymbolicLink(link1, PathFragment.create(".."));
     // Create symlink with directory target, absolute path.
-    Path link2 = fs.getPath(scratchRoot).getRelative("link2");
-    fs.createSymbolicLink(link2, fs.getPath(scratchRoot).getRelative("link1").asFragment());
+    Path link2 = scratchRoot.getRelative("link2");
+    fs.createSymbolicLink(link2, scratchRoot.getRelative("link1").asFragment());
     // Create scratch files that'll be symlink targets.
     testUtil.scratchFile("foo.txt", "hello");
     testUtil.scratchFile("bar.txt", "hello");
     // Create symlink with file target, relative path.
-    Path link3 = fs.getPath(scratchRoot).getRelative("link3");
+    Path link3 = scratchRoot.getRelative("link3");
     fs.createSymbolicLink(link3, PathFragment.create("foo.txt"));
     // Create symlink with file target, absolute path.
-    Path link4 = fs.getPath(scratchRoot).getRelative("link4");
-    fs.createSymbolicLink(link4, fs.getPath(scratchRoot).getRelative("bar.txt").asFragment());
+    Path link4 = scratchRoot.getRelative("link4");
+    fs.createSymbolicLink(link4, scratchRoot.getRelative("bar.txt").asFragment());
     // Assert that link1 and link2 are true junctions and have the right contents.
     for (Path p : ImmutableList.of(link1, link2)) {
       assertThat(WindowsFileSystem.isSymlinkOrJunction(new File(p.getPathString()))).isTrue();
@@ -369,4 +369,25 @@
 
     assertThat(juncPath.readSymbolicLink()).isEqualTo(dirPath.asFragment());
   }
+
+  private static String invertCharacterCasing(String s) {
+    char[] a = s.toCharArray();
+    for (int i = 0; i < a.length; ++i) {
+      char c = a[i];
+      a[i] = Character.isUpperCase(c) ? Character.toLowerCase(c) : Character.toUpperCase(c);
+    }
+    return new String(a);
+  }
+
+  @Test
+  public void testGetCorrectCasing() throws Exception {
+    String rootStr = scratchRoot.getPathString();
+    String inverseRootStr = invertCharacterCasing(rootStr);
+    Path inverseRoot = fs.getPath(inverseRootStr);
+    assertThat(inverseRootStr).isNotEqualTo(rootStr);
+    assertThat(inverseRoot).isEqualTo(scratchRoot);
+    Path correctCasing = fs.getCorrectCasingForTesting(inverseRoot);
+    assertThat(correctCasing).isEqualTo(scratchRoot);
+    assertThat(correctCasing.getPathString()).isNotEqualTo(inverseRootStr);
+  }
 }
diff --git a/src/test/native/windows/file_test.cc b/src/test/native/windows/file_test.cc
index 49ce133..de3c8af 100644
--- a/src/test/native/windows/file_test.cc
+++ b/src/test/native/windows/file_test.cc
@@ -452,8 +452,64 @@
   ASSERT_NORMALIZE("c:\\", "c:\\");
   ASSERT_NORMALIZE("c:\\..//foo/./bar/", "c:\\foo\\bar");
   ASSERT_NORMALIZE("../foo", "..\\foo");
+  ASSERT_NORMALIZE("../foo\\", "..\\foo");
+  ASSERT_NORMALIZE("../foo\\\\", "..\\foo");
+  ASSERT_NORMALIZE("..//foo\\\\", "..\\foo");
 #undef ASSERT_NORMALIZE
 }
 
+static void GetTestTempDirWithCorrectCasing(std::wstring* result,
+                                            std::wstring* result_inverted) {
+  static constexpr size_t kMaxPath = 0x8000;
+  wchar_t buf[kMaxPath];
+  ASSERT_GT(GetEnvironmentVariableW(L"TEST_TMPDIR", buf, kMaxPath), 0);
+  HANDLE h = CreateFileW(buf, 0,
+                         FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+                         NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
+  ASSERT_NE(h, INVALID_HANDLE_VALUE);
+  ASSERT_GT(GetFinalPathNameByHandleW(h, buf, kMaxPath, 0), 0);
+  CloseHandle(h);
+  *result = RemoveUncPrefixMaybe(buf);
+
+  for (wchar_t* c = buf; *c; ++c) {
+    if (*c >= L'A' && *c <= L'Z') {
+      *c = L'a' + *c - L'A';
+    } else if (*c >= L'a' && *c <= L'z') {
+      *c = L'A' + *c - L'a';
+    }
+  }
+  *result_inverted = RemoveUncPrefixMaybe(buf);
+}
+
+TEST(FileTests, TestGetCorrectCase) {
+  EXPECT_EQ(GetCorrectCasing(L""), L"");
+  EXPECT_EQ(GetCorrectCasing(L"d"), L"");
+  EXPECT_EQ(GetCorrectCasing(L"d:"), L"");
+
+  EXPECT_EQ(GetCorrectCasing(L"d:\\"), L"D:\\");
+  EXPECT_EQ(GetCorrectCasing(L"d:/"), L"D:\\");
+  EXPECT_EQ(GetCorrectCasing(L"d:\\\\"), L"D:\\");
+  EXPECT_EQ(GetCorrectCasing(L"d://"), L"D:\\");
+  EXPECT_EQ(GetCorrectCasing(L"d:\\\\\\"), L"D:\\");
+  EXPECT_EQ(GetCorrectCasing(L"d:///"), L"D:\\");
+
+  EXPECT_EQ(GetCorrectCasing(L"A:/Non:Existent"), L"A:\\Non:Existent");
+
+  EXPECT_EQ(GetCorrectCasing(L"A:\\\\Non:Existent"), L"A:\\Non:Existent");
+  EXPECT_EQ(GetCorrectCasing(L"A:\\Non:Existent\\."), L"A:\\Non:Existent");
+  EXPECT_EQ(GetCorrectCasing(L"A:\\Non:Existent\\..\\.."), L"A:\\");
+  EXPECT_EQ(GetCorrectCasing(L"A:\\Non:Existent\\.\\"), L"A:\\Non:Existent");
+
+  std::wstring tmp, tmp_inverse;
+  GetTestTempDirWithCorrectCasing(&tmp, &tmp_inverse);
+  ASSERT_GT(tmp.size(), 0);
+  ASSERT_NE(tmp, tmp_inverse);
+  EXPECT_EQ(GetCorrectCasing(tmp), tmp);
+  EXPECT_EQ(GetCorrectCasing(tmp_inverse), tmp);
+  EXPECT_EQ(GetCorrectCasing(tmp_inverse + L"\\"), tmp);
+  EXPECT_EQ(GetCorrectCasing(tmp_inverse + L"\\Does\\\\Not\\./Exist"),
+            tmp + L"\\Does\\Not\\Exist");
+}
+
 }  // namespace windows
 }  // namespace bazel