Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows: implement getCorrectCasing #9435

Closed
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,33 @@ public static boolean deletePath(String path) throws IOException {
throw new IOException(String.format("Cannot delete path '%s': %s", path, error[0]));
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please a comment here that explains what "correct" refers to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/**
* 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]);
}
}
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));
env->SetObjectArrayElement(
result_holder, 0,
env->NewString(reinterpret_cast<const jchar*>(result.c_str()),
result.size()));
}
56 changes: 56 additions & 0 deletions src/main/native/windows/file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -781,5 +781,61 @@ bool GetCwd(std::wstring* result, DWORD* err_code) {
}
}

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of implicit knowledge in this code which makes it hard to read, e.g. where does the 7 come from?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this from UNC (if so make it conditional)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment.

// 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
17 changes: 17 additions & 0 deletions src/main/native/windows/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,23 @@ 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 '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

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);
}
}
55 changes: 55 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,63 @@ 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""), 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