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