Skip to content

Commit

Permalink
Windows: implement getCorrectCasing
Browse files Browse the repository at this point in the history
Implement getCorrectCasing: a function that
normalizes a path and returns the case-correct
version of it.

See bazelbuild#8799
  • Loading branch information
laszlocsomor committed Sep 25, 2019
1 parent 7fa0dd2 commit 4a83aee
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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]);
}
}
11 changes: 11 additions & 0 deletions src/main/native/windows/file-jni.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<const jchar*>(result.c_str()),
result.size()));
}
41 changes: 41 additions & 0 deletions src/main/native/windows/file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<wchar_t[]> 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
18 changes: 18 additions & 0 deletions src/main/native/windows/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -238,24 +238,24 @@ 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
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);
}
Expand All @@ -269,46 +269,46 @@ 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();
assertThat(q1.exists()).isFalse();

// 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();
Expand Down Expand Up @@ -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);
}
}
71 changes: 71 additions & 0 deletions src/test/native/windows/file_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 4a83aee

Please sign in to comment.