From 4a83aee6a50afe87f06d19973a8589c8a6ffab3c Mon Sep 17 00:00:00 2001 From: Laszlo Csomor Date: Tue, 24 Sep 2019 17:04:17 +0200 Subject: [PATCH] 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 --- .../build/lib/windows/WindowsFileSystem.java | 5 ++ .../windows/jni/WindowsFileOperations.java | 12 ++++ src/main/native/windows/file-jni.cc | 11 +++ src/main/native/windows/file.cc | 41 +++++++++++ src/main/native/windows/file.h | 18 +++++ .../lib/windows/WindowsFileSystemTest.java | 69 +++++++++++------- src/test/native/windows/file_test.cc | 71 +++++++++++++++++++ 7 files changed, 203 insertions(+), 24 deletions(-) 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 77b78fdad5cc17..2ad60e247fc461 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 @@ private static DosFileAttributes getAttribs(File file, boolean followSymlinks) 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 ba82a4f98bfb8b..efb5df6b4ab600 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 nativeReadSymlinkOrJunction( 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,14 @@ public static boolean deletePath(String path) throws IOException { throw new IOException(String.format("Cannot delete path '%s': %s", path, error[0])); } } + + public static String getCorrectCasing(String path) throws IOException { + WindowsJniLoader.loadJni(); + String[] result = new String[] {null}; + nativeGetCorrectCasing(path, result); + if (result[0].isEmpty()) { + throw new IOException(String.format("Path is not Windows-style: '%s'", path)); + } + return removeUncPrefixAndUseSlashes(result[0]); + } } diff --git a/src/main/native/windows/file-jni.cc b/src/main/native/windows/file-jni.cc index 63d81d8775596d..3d6a1e3f84044e 100644 --- a/src/main/native/windows/file-jni.cc +++ b/src/main/native/windows/file-jni.cc @@ -141,3 +141,14 @@ Java_com_google_devtools_build_lib_windows_jni_WindowsFileOperations_nativeDelet } 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, false)); + env->SetObjectArrayElement( + result_holder, 0, + env->NewString(reinterpret_cast(result.c_str()), + result.size())); +} diff --git a/src/main/native/windows/file.cc b/src/main/native/windows/file.cc index cc79ead588522f..e88e3748c86ba2 100644 --- a/src/main/native/windows/file.cc +++ b/src/main/native/windows/file.cc @@ -781,5 +781,46 @@ bool GetCwd(std::wstring* result, DWORD* err_code) { } } +std::wstring GetCorrectCasing(const std::wstring& abs_path, bool with_unc) { + if (!HasDriveSpecifierPrefix(abs_path.c_str())) { + return L""; + } + std::wstring path = Normalize(abs_path); + std::unique_ptr result(new wchar_t[4 + path.size() + 1]); + wcscpy(result.get(), L"\\\\?\\"); + wcscpy(result.get() + 4, path.c_str()); + result[4] = towupper(result[4]); + wchar_t* start = result.get() + 7; + while (true) { + wchar_t* seg_end = wcschr(start, L'\\'); + if (seg_end) { + *seg_end = 0; + } + WIN32_FIND_DATAW metadata; + HANDLE handle = FindFirstFileW(result.get(), &metadata); + if (handle != INVALID_HANDLE_VALUE) { + // Found the child, metadata.cFileName has the correct casing. + wcscpy(start, metadata.cFileName); + FindClose(handle); + if (seg_end) { + *seg_end = L'\\'; + start = seg_end + 1; + } + } else { + // Non-existent path, leave the rest of it unchanged. + if (seg_end) { + *seg_end = L'\\'; + } + break; + } + if (!seg_end) { + // This was the last segment. + break; + } + } + + return result.get() + (with_unc ? 0 : 4); +} + } // namespace windows } // namespace bazel diff --git a/src/main/native/windows/file.h b/src/main/native/windows/file.h index f8651e6ed979eb..d901ebd6d8a81f 100644 --- a/src/main/native/windows/file.h +++ b/src/main/native/windows/file.h @@ -195,6 +195,24 @@ std::wstring Normalize(const std::wstring& p); 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 'with_unc' is true, the result will have an UNC prefix. +// If 'abs_path' is null or empty or not absolute, the result is empty. +std::wstring GetCorrectCasing(const std::wstring& abs_path, bool with_unc); + } // 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 3de77fe65ef946..1669fa5b133fd9 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 @@ public void testShortPathResolution() throws Exception { 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 @@ public void testUnresolvableShortPathWhichIsThenCreated() throws Exception { 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 @@ public void testUnresolvableShortPathWhichIsThenCreated() throws Exception { */ @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 @@ public void testShortPathResolvesToDifferentPathsOverTime() throws Exception { // 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 @@ public void testReadJunction() throws Exception { 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 49ce1339d54781..b4985abbf8c390 100644 --- a/src/test/native/windows/file_test.cc +++ b/src/test/native/windows/file_test.cc @@ -452,8 +452,79 @@ TEST(FileTests, TestNormalize) { 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); + *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"", false), L""); + EXPECT_EQ(GetCorrectCasing(L"", true), L""); + EXPECT_EQ(GetCorrectCasing(L"d", false), L""); + EXPECT_EQ(GetCorrectCasing(L"d", true), L""); + EXPECT_EQ(GetCorrectCasing(L"d:", false), L""); + EXPECT_EQ(GetCorrectCasing(L"d:", true), L""); + + EXPECT_EQ(GetCorrectCasing(L"d:\\", false), L"D:\\"); + EXPECT_EQ(GetCorrectCasing(L"d:/", false), L"D:\\"); + EXPECT_EQ(GetCorrectCasing(L"d:\\\\", false), L"D:\\"); + EXPECT_EQ(GetCorrectCasing(L"d://", false), L"D:\\"); + EXPECT_EQ(GetCorrectCasing(L"d:\\\\\\", false), L"D:\\"); + EXPECT_EQ(GetCorrectCasing(L"d:///", false), L"D:\\"); + + EXPECT_EQ(GetCorrectCasing(L"d:///", true), L"\\\\?\\D:\\"); + + EXPECT_EQ(GetCorrectCasing(L"A:/Non:Existent", false), L"A:\\Non:Existent"); + EXPECT_EQ(GetCorrectCasing(L"A:/Non:Existent", true), L"\\\\?\\A:\\Non:Existent"); + + EXPECT_EQ(GetCorrectCasing(L"A:\\\\Non:Existent", false), L"A:\\Non:Existent"); + EXPECT_EQ(GetCorrectCasing(L"A:\\\\Non:Existent", true), L"\\\\?\\A:\\Non:Existent"); + + EXPECT_EQ(GetCorrectCasing(L"A:\\Non:Existent\\.", false), L"A:\\Non:Existent"); + EXPECT_EQ(GetCorrectCasing(L"A:\\Non:Existent\\.", true), L"\\\\?\\A:\\Non:Existent"); + + EXPECT_EQ(GetCorrectCasing(L"A:\\Non:Existent\\..\\..", false), L"A:\\"); + EXPECT_EQ(GetCorrectCasing(L"A:\\Non:Existent\\..\\..", true), L"\\\\?\\A:\\"); + + EXPECT_EQ(GetCorrectCasing(L"A:\\Non:Existent\\.\\", false), L"A:\\Non:Existent"); + EXPECT_EQ(GetCorrectCasing(L"A:\\Non:Existent\\.\\", true), 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, false), tmp); + EXPECT_EQ(GetCorrectCasing(tmp_inverse, false), tmp); + EXPECT_EQ(GetCorrectCasing(tmp_inverse, true), L"\\\\?\\" + tmp); + + EXPECT_EQ(GetCorrectCasing(tmp_inverse + L"\\", false), tmp); + + EXPECT_EQ(GetCorrectCasing(tmp_inverse + L"\\Does\\\\Not\\./Exist", false), + tmp + L"\\Does\\Not\\Exist"); +} + } // namespace windows } // namespace bazel