Windows, JNI: implement nativeGetLongPath
Implement a JNI method that can resolve 8dot3
style paths. This is necessary because we need to
be able to compare 8dot3 style paths and long
paths [1], and because implementing this correctly
in Java seems to be impossible [2].
This change also adds tests for the JNI isJunction
implementation.
See https://github.com/bazelbuild/bazel/issues/2101
[1] https://github.com/bazelbuild/bazel/issues/2145
[2] https://github.com/bazelbuild/bazel/issues/2145#issuecomment-266766716
--
PiperOrigin-RevId: 142123750
MOS_MIGRATED_REVID=142123750
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileOperations.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileOperations.java
index 0eb287e..d95f902 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileOperations.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileOperations.java
@@ -30,6 +30,8 @@
static native int nativeIsJunction(String path, String[] error);
+ static native boolean nativeGetLongPath(String path, String[] result, String[] error);
+
/** Determines whether `path` is a junction point or directory symlink. */
public static boolean isJunction(String path) throws IOException {
WindowsJniLoader.loadJni();
@@ -44,7 +46,37 @@
}
}
+ /**
+ * Returns the long path associated with the input `path`.
+ *
+ * <p>This method resolves all 8dot3 style components of the path and returns the long format. For
+ * example, if the input is "C:/progra~1/micros~1" the result may be "C:\Program Files\Microsoft
+ * Visual Studio 14.0". The returned path is Windows-style in that it uses backslashes, even if
+ * the input uses forward slashes.
+ *
+ * <p>May return an UNC path if `path` or its resolution is sufficiently long.
+ *
+ * @throws IOException if the `path` is not found or some other I/O error occurs
+ */
+ public static String getLongPath(String path) throws IOException {
+ String[] result = new String[] {null};
+ String[] error = new String[] {null};
+ if (nativeGetLongPath(asLongPath(path), result, error)) {
+ return result[0];
+ } else {
+ throw new IOException(error[0]);
+ }
+ }
+
+ /**
+ * Returns a Windows-style path suitable to pass to Widechar Win32 API functions.
+ *
+ * <p>Returns an UNC path if `path` is longer than `MAX_PATH` (in <windows.h>). If it's shorter or
+ * is already an UNC path, then this method returns `path` itself.
+ */
static String asLongPath(String path) {
- return "\\\\?\\" + path.replace('/', '\\');
+ return path.length() > 260 && !path.startsWith("\\\\?\\")
+ ? ("\\\\?\\" + path.replace('/', '\\'))
+ : path;
}
}
diff --git a/src/main/native/windows_file_operations.cc b/src/main/native/windows_file_operations.cc
index 9f74293..137121a 100644
--- a/src/main/native/windows_file_operations.cc
+++ b/src/main/native/windows_file_operations.cc
@@ -15,13 +15,18 @@
#include <jni.h>
#include <windows.h>
-#include <memory>
#include <string>
+#include <type_traits> // static_assert
#include "src/main/native/windows_util.h"
namespace windows_util {
+// Ensure we can safely cast (const) jchar* to LP(C)WSTR.
+// This is true with MSVC but usually not with GCC.
+static_assert(sizeof(jchar) == sizeof(WCHAR),
+ "jchar and WCHAR should be the same size");
+
// Keep in sync with j.c.g.devtools.build.lib.windows.WindowsFileOperations
enum {
IS_JUNCTION_YES = 0,
@@ -45,7 +50,7 @@
// created using "mklink" instead of "mklink /d", as such symlinks don't
// behave the same way as directories (e.g. they can't be listed)
// - IS_JUNCTION_ERROR, if `path` doesn't exist or some error occurred
-static int IsJunctionOrDirectorySymlink(const wchar_t* path) {
+static int IsJunctionOrDirectorySymlink(LPCWSTR path) {
DWORD attrs = GetFileAttributesW(path);
if (attrs == INVALID_FILE_ATTRIBUTES) {
return IS_JUNCTION_ERROR;
@@ -59,26 +64,59 @@
}
}
+static void MaybeReportLastError(string reason, JNIEnv* env,
+ jobjectArray error_msg_holder) {
+ if (error_msg_holder != nullptr &&
+ env->GetArrayLength(error_msg_holder) > 0) {
+ std::string error_str = windows_util::GetLastErrorString(reason);
+ jstring error_msg = env->NewStringUTF(error_str.c_str());
+ env->SetObjectArrayElement(error_msg_holder, 0, error_msg);
+ }
+}
+
} // namespace windows_util
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeIsJunction(
JNIEnv* env, jclass clazz, jstring path, jobjectArray error_msg_holder) {
bool report_error =
- error_msg_holder != NULL && env->GetArrayLength(error_msg_holder) > 0;
- std::unique_ptr<wchar_t[]> long_path(
- windows_util::JstringToWstring(env, path));
- int result = windows_util::IsJunctionOrDirectorySymlink(long_path.get());
+ error_msg_holder != nullptr && env->GetArrayLength(error_msg_holder) > 0;
+ const jchar* wpath = env->GetStringChars(path, nullptr);
+ int result = windows_util::IsJunctionOrDirectorySymlink((LPCWSTR)wpath);
+ env->ReleaseStringChars(path, wpath);
if (result == windows_util::IS_JUNCTION_ERROR && report_error) {
- // Getting the string's characters again in UTF8 encoding is probably
- // easier than converting `long_path` using `wcstombs(3)`.
- const char* path_cstr = env->GetStringUTFChars(path, NULL);
- std::string error_str = windows_util::GetLastErrorString(
- std::string("GetFileAttributes(") + std::string(path_cstr) +
- std::string(")"));
+ // Getting the string's characters again in UTF8 encoding is
+ // easier than converting `wpath` using `wcstombs(3)`.
+ const char* path_cstr = env->GetStringUTFChars(path, nullptr);
+ windows_util::MaybeReportLastError(std::string("GetFileAttributes(") +
+ std::string(path_cstr) +
+ std::string(")"),
+ env, error_msg_holder);
env->ReleaseStringUTFChars(path, path_cstr);
- jstring error_msg = env->NewStringUTF(error_str.c_str());
- env->SetObjectArrayElement(error_msg_holder, 0, error_msg);
}
return result;
}
+
+extern "C" JNIEXPORT jboolean JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeGetLongPath(
+ JNIEnv* env, jclass clazz, jstring path, jobjectArray result_holder,
+ jobjectArray error_msg_holder) {
+ const jchar* cpath = nullptr;
+ cpath = env->GetStringChars(path, nullptr);
+ jchar result[0x8010] = {0}; // 32K max size + UNC prefix + some safety buffer
+ DWORD len = GetLongPathNameW((LPCWSTR)cpath, (LPWSTR)result, 0x8010);
+ env->ReleaseStringChars(path, cpath);
+ if (len > 0 && len < 0x8010) {
+ env->SetObjectArrayElement(result_holder, 0,
+ env->NewString((const jchar*)result, len));
+ return JNI_TRUE;
+ } else {
+ const char* path_cstr = env->GetStringUTFChars(path, nullptr);
+ windows_util::MaybeReportLastError(std::string("GetLongPathNameW(") +
+ std::string(path_cstr) +
+ std::string(")"),
+ env, error_msg_holder);
+ env->ReleaseStringUTFChars(path, path_cstr);
+ return JNI_FALSE;
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/WindowsFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/WindowsFileSystemTest.java
index e0b62f1..8f25ff8 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/WindowsFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/WindowsFileSystemTest.java
@@ -200,4 +200,23 @@
linkPath.toPath(), WindowsFileSystem.symlinkOpts(/* followSymlinks */ true)))
.isFalse();
}
+
+ @Test
+ public void testIsJunctionHandlesFilesystemChangesCorrectly() throws Exception {
+ File longPath =
+ testUtil.scratchFile("target\\helloworld.txt", "hello").toAbsolutePath().toFile();
+ File shortPath = new File(longPath.getParentFile(), "hellow~1.txt");
+ assertThat(WindowsFileSystem.isJunction(longPath)).isFalse();
+ assertThat(WindowsFileSystem.isJunction(shortPath)).isFalse();
+
+ assertThat(longPath.delete()).isTrue();
+ testUtil.createJunctions(ImmutableMap.of("target\\helloworld.txt", "target"));
+ assertThat(WindowsFileSystem.isJunction(longPath)).isTrue();
+ assertThat(WindowsFileSystem.isJunction(shortPath)).isTrue();
+
+ assertThat(longPath.delete()).isTrue();
+ assertThat(longPath.mkdir()).isTrue();
+ assertThat(WindowsFileSystem.isJunction(longPath)).isFalse();
+ assertThat(WindowsFileSystem.isJunction(shortPath)).isFalse();
+ }
}
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileOperationsTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileOperationsTest.java
index 84f7752..773daee 100644
--- a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileOperationsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileOperationsTest.java
@@ -159,4 +159,99 @@
linkPath.toPath(), WindowsFileSystem.symlinkOpts(/* followSymlinks */ true)))
.isFalse();
}
+
+ @Test
+ public void testIsJunctionHandlesFilesystemChangesCorrectly() throws Exception {
+ File helloFile =
+ testUtil.scratchFile("target\\helloworld.txt", "hello").toAbsolutePath().toFile();
+
+ // Assert that a file is identified as not a junction.
+ String longPath = helloFile.getAbsolutePath();
+ String shortPath = new File(helloFile.getParentFile(), "hellow~1.txt").getAbsolutePath();
+ assertThat(WindowsFileOperations.isJunction(longPath)).isFalse();
+ assertThat(WindowsFileOperations.isJunction(shortPath)).isFalse();
+
+ // Assert that after deleting the file and creating a junction with the same path, it is
+ // identified as a junction.
+ assertThat(helloFile.delete()).isTrue();
+ testUtil.createJunctions(ImmutableMap.of("target\\helloworld.txt", "target"));
+ assertThat(WindowsFileOperations.isJunction(longPath)).isTrue();
+ assertThat(WindowsFileOperations.isJunction(shortPath)).isTrue();
+
+ // Assert that after deleting the file and creating a directory with the same path, it is
+ // identified as not a junction.
+ assertThat(helloFile.delete()).isTrue();
+ assertThat(helloFile.mkdir()).isTrue();
+ assertThat(WindowsFileOperations.isJunction(longPath)).isFalse();
+ assertThat(WindowsFileOperations.isJunction(shortPath)).isFalse();
+ }
+
+ @Test
+ public void testGetLongPath() throws Exception {
+ File foo = testUtil.scratchDir("foo").toAbsolutePath().toFile();
+ assertThat(foo.exists()).isTrue();
+ assertThat(WindowsFileOperations.getLongPath(foo.getAbsolutePath())).endsWith("foo");
+
+ String longPath = foo.getAbsolutePath() + "\\will.exist\\helloworld.txt";
+ String shortPath = foo.getAbsolutePath() + "\\will~1.exi\\hellow~1.txt";
+
+ // Assert that the long path resolution fails for non-existent file.
+ try {
+ WindowsFileOperations.getLongPath(longPath);
+ fail("expected to throw");
+ } catch (IOException e) {
+ assertThat(e.getMessage()).contains("GetLongPathName");
+ }
+ try {
+ WindowsFileOperations.getLongPath(shortPath);
+ fail("expected to throw");
+ } catch (IOException e) {
+ assertThat(e.getMessage()).contains("GetLongPathName");
+ }
+
+ // Create the file, assert that long path resolution works and is correct.
+ File helloFile =
+ testUtil.scratchFile("foo/will.exist/helloworld.txt", "hello").toAbsolutePath().toFile();
+ assertThat(helloFile.getAbsolutePath()).isEqualTo(longPath);
+ assertThat(helloFile.exists()).isTrue();
+ assertThat(new File(longPath).exists()).isTrue();
+ assertThat(new File(shortPath).exists()).isTrue();
+ assertThat(WindowsFileOperations.getLongPath(longPath)).endsWith("will.exist\\helloworld.txt");
+ assertThat(WindowsFileOperations.getLongPath(shortPath)).endsWith("will.exist\\helloworld.txt");
+
+ // Delete the file and the directory, assert that long path resolution fails for them.
+ assertThat(helloFile.delete()).isTrue();
+ assertThat(helloFile.getParentFile().delete()).isTrue();
+ try {
+ WindowsFileOperations.getLongPath(longPath);
+ fail("expected to throw");
+ } catch (IOException e) {
+ assertThat(e.getMessage()).contains("GetLongPathName");
+ }
+ try {
+ WindowsFileOperations.getLongPath(shortPath);
+ fail("expected to throw");
+ } catch (IOException e) {
+ assertThat(e.getMessage()).contains("GetLongPathName");
+ }
+
+ // Create the directory and file with different names, but same 8dot3 names, assert that the
+ // resolution is still correct.
+ helloFile =
+ testUtil
+ .scratchFile("foo/will.exist_again/hellowelt.txt", "hello")
+ .toAbsolutePath()
+ .toFile();
+ assertThat(new File(shortPath).exists()).isTrue();
+ assertThat(WindowsFileOperations.getLongPath(shortPath))
+ .endsWith("will.exist_again\\hellowelt.txt");
+ assertThat(WindowsFileOperations.getLongPath(foo + "\\will.exist_again\\hellowelt.txt"))
+ .endsWith("will.exist_again\\hellowelt.txt");
+ try {
+ WindowsFileOperations.getLongPath(longPath);
+ fail("expected to throw");
+ } catch (IOException e) {
+ assertThat(e.getMessage()).contains("GetLongPathName");
+ }
+ }
}