aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar Laszlo Csomor <laszlocsomor@google.com>2017-02-15 13:07:52 +0000
committerGravatar Dmitry Lomov <dslomov@google.com>2017-02-15 14:47:42 +0000
commitd7d4def945b38133c43e3df3ea7c450b229d2bfb (patch)
treeb370d2c91c525a692e4eee01e2fbe6331e7e1df2 /src
parent2a0bb37397e1354eaf06b7697913f71443332175 (diff)
Bazel client, JNI, Windows: impl. CreateJunction
Implement a CreateJunction function in the Windows JNI library. Also move a bit of code from file_windows to the JNI library, where it is (also) needed. This implementation is an improved version of `blaze_util::SymlinkDirectories` in blaze_util_windows: this version handles Windows paths as `name` and `target`, and performs more validation (e.g. on the length of `target`), plus has more comments explaining the logic. In a subsequent change I'll start using this new function in blaze_util_windows. This method will also be helpful in tests: we will no longer have to shell out to mklink. See https://github.com/bazelbuild/bazel/issues/2107 -- Change-Id: I7e9b085fdc2ba47be83da5319bded02bd323e71b Reviewed-on: https://cr.bazel.build/8892 PiperOrigin-RevId: 147585207 MOS_MIGRATED_REVID=147585207
Diffstat (limited to 'src')
-rw-r--r--src/main/cpp/util/file_windows.cc7
-rw-r--r--src/main/native/windows_file_operations.cc121
-rw-r--r--src/main/native/windows_file_operations.h18
-rw-r--r--src/test/cpp/util/BUILD24
-rw-r--r--src/test/cpp/util/windows_test_util.cc3
-rw-r--r--src/test/cpp/util/windows_test_util.h3
-rw-r--r--src/test/native/BUILD15
-rw-r--r--src/test/native/windows_file_operations_test.cc111
8 files changed, 287 insertions, 15 deletions
diff --git a/src/main/cpp/util/file_windows.cc b/src/main/cpp/util/file_windows.cc
index 9d22444dc8..e2e12950eb 100644
--- a/src/main/cpp/util/file_windows.cc
+++ b/src/main/cpp/util/file_windows.cc
@@ -35,6 +35,7 @@ using std::pair;
using std::string;
using std::unique_ptr;
using std::wstring;
+using windows_util::HasUncPrefix;
// Returns the current working directory as a Windows path.
// The result may have a UNC prefix.
@@ -101,12 +102,6 @@ static bool HasDriveSpecifierPrefix(const char_type* ch) {
return CharTraits<char_type>::IsAlpha(ch[0]) && ch[1] == ':';
}
-template <typename char_type>
-static bool HasUncPrefix(const char_type* path) {
- return path[0] == '\\' && (path[1] == '\\' || path[1] == '?') &&
- (path[2] == '.' || path[2] == '?') && path[3] == '\\';
-}
-
static void AddUncPrefixMaybe(wstring* path) {
if (path->size() >= MAX_PATH && !HasUncPrefix(path->c_str())) {
*path = wstring(L"\\\\?\\") + *path;
diff --git a/src/main/native/windows_file_operations.cc b/src/main/native/windows_file_operations.cc
index b5780b2c86..0b02fb9238 100644
--- a/src/main/native/windows_file_operations.cc
+++ b/src/main/native/windows_file_operations.cc
@@ -15,13 +15,17 @@
#include <windows.h>
#include <memory>
+#include <sstream>
+#include <string>
#include "src/main/native/windows_file_operations.h"
#include "src/main/native/windows_util.h"
namespace windows_util {
+using std::string;
using std::unique_ptr;
+using std::wstring;
int IsJunctionOrDirectorySymlink(const WCHAR* path) {
DWORD attrs = ::GetFileAttributesW(path);
@@ -60,4 +64,121 @@ HANDLE OpenDirectory(const WCHAR* path, bool read_write) {
/* hTemplateFile */ NULL);
}
+#pragma pack(push, 4)
+typedef struct _JunctionDescription {
+ typedef struct _Header {
+ DWORD ReparseTag;
+ WORD ReparseDataLength;
+ WORD Reserved;
+ } Header;
+
+ typedef struct _WriteDesc {
+ WORD SubstituteNameOffset;
+ WORD SubstituteNameLength;
+ WORD PrintNameOffset;
+ WORD PrintNameLength;
+ } WriteDesc;
+
+ Header header;
+ WriteDesc write;
+ WCHAR PathBuffer[ANYSIZE_ARRAY];
+} JunctionDescription;
+#pragma pack(pop)
+
+string CreateJunction(const wstring& junction_name,
+ const wstring& junction_target) {
+ const wstring target = HasUncPrefix(junction_target.c_str())
+ ? junction_target.substr(4)
+ : junction_target;
+ // The entire JunctionDescription cannot be larger than
+ // MAXIMUM_REPARSE_DATA_BUFFER_SIZE bytes.
+ //
+ // The structure's layout is:
+ // [JunctionDescription::Header]
+ // [JunctionDescription::WriteDesc]
+ // ---- start of JunctionDescription::PathBuffer ----
+ // [4 WCHARs] : "\??\" prefix
+ // [target.size() WCHARs] : junction target name
+ // [1 WCHAR] : null-terminator
+ // [target.size() WCHARs] : junction target displayed name
+ // [1 WCHAR] : null-terminator
+ // The sum of these must not exceed MAXIMUM_REPARSE_DATA_BUFFER_SIZE.
+ // We can rearrange this to get the limit for target.size().
+ static const size_t kMaxJunctionTargetLen =
+ ((MAXIMUM_REPARSE_DATA_BUFFER_SIZE - sizeof(JunctionDescription::Header) -
+ sizeof(JunctionDescription::WriteDesc) -
+ /* one "\??\" prefix */ sizeof(WCHAR) * 4 -
+ /* two null terminators */ sizeof(WCHAR) * 2) /
+ /* two copies of the string are stored */ 2) /
+ sizeof(WCHAR);
+ if (target.size() > kMaxJunctionTargetLen) {
+ std::stringstream error;
+ error << "junction target is too long (" << target.size()
+ << " characters, limit: " << kMaxJunctionTargetLen << ")";
+ return error.str();
+ }
+ const wstring name = HasUncPrefix(junction_name.c_str())
+ ? junction_name
+ : (wstring(L"\\\\?\\") + junction_name);
+
+ // Junctions are directories, so create one
+ if (!::CreateDirectoryW(name.c_str(), NULL)) {
+ return string("CreateDirectoryW failed");
+ }
+
+ AutoHandle handle(OpenDirectory(name.c_str(), true));
+ if (!handle.IsValid()) {
+ return string("OpenDirectory failed");
+ }
+
+ char reparse_buffer_bytes[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
+ JunctionDescription* reparse_buffer =
+ reinterpret_cast<JunctionDescription*>(reparse_buffer_bytes);
+ memset(reparse_buffer_bytes, 0, MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
+
+ // "\??\" is meaningful to the kernel, it's a synomym for the "\DosDevices\"
+ // object path. (NOT to be confused with "\\?\" which is meaningful for the
+ // Win32 API.) We need to use this prefix to tell the kernel where the reparse
+ // point is pointing to.
+ memcpy(reparse_buffer->PathBuffer, L"\\??\\", 4 * sizeof(WCHAR));
+ memcpy(reparse_buffer->PathBuffer + 4, target.c_str(),
+ target.size() * sizeof(WCHAR));
+
+ // In addition to their target, junctions also have another string which is a
+ // user-visible name of where the junction points, as listed by "dir". This
+ // can be any string and won't affect the usability of the junction.
+ // MKLINK uses the target path without the "\??\" prefix as the display name,
+ // so let's do that here too. This is also in line with how UNIX behaves.
+ // Using a dummy or fake display name would be pure evil, it would make the
+ // output of `dir` look like:
+ // 2017-01-18 01:37 PM <JUNCTION> juncname [dummy string]
+ memcpy(reparse_buffer->PathBuffer + 4 + target.size() + 1, target.c_str(),
+ target.size() * sizeof(WCHAR));
+
+ reparse_buffer->write.SubstituteNameOffset = 0;
+ reparse_buffer->write.SubstituteNameLength =
+ (4 + target.size()) * sizeof(WCHAR);
+ reparse_buffer->write.PrintNameOffset =
+ reparse_buffer->write.SubstituteNameLength +
+ /* null-terminator */ sizeof(WCHAR);
+ reparse_buffer->write.PrintNameLength = target.size() * sizeof(WCHAR);
+
+ reparse_buffer->header.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
+ reparse_buffer->header.ReparseDataLength =
+ sizeof(JunctionDescription::WriteDesc) +
+ reparse_buffer->write.SubstituteNameLength +
+ reparse_buffer->write.PrintNameLength +
+ /* 2 null-terminators */ (2 * sizeof(WCHAR));
+ reparse_buffer->header.Reserved = 0;
+
+ DWORD bytes_returned;
+ if (!::DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, reparse_buffer,
+ reparse_buffer->header.ReparseDataLength +
+ sizeof(JunctionDescription::Header),
+ NULL, 0, &bytes_returned, NULL)) {
+ return string("DeviceIoControl(FSCTL_SET_REPARSE_POINT) failed");
+ }
+ return "";
+}
+
} // namespace windows_util
diff --git a/src/main/native/windows_file_operations.h b/src/main/native/windows_file_operations.h
index ded07b0a1f..9b2c76837a 100644
--- a/src/main/native/windows_file_operations.h
+++ b/src/main/native/windows_file_operations.h
@@ -17,10 +17,19 @@
#include <windows.h>
#include <memory>
+#include <string>
namespace windows_util {
+using std::string;
using std::unique_ptr;
+using std::wstring;
+
+template <typename char_type>
+bool HasUncPrefix(const char_type* path) {
+ return path[0] == '\\' && (path[1] == '\\' || path[1] == '?') &&
+ (path[2] == '.' || path[2] == '?') && path[3] == '\\';
+}
// Keep in sync with j.c.g.devtools.build.lib.windows.WindowsFileOperations
enum {
@@ -61,6 +70,15 @@ bool GetLongPath(const WCHAR* path, unique_ptr<WCHAR[]>* result);
// otherwise only for reading.
HANDLE OpenDirectory(const WCHAR* path, bool read_write);
+// Creates a junction at `name`, pointing to `target`.
+// Returns the empty string upon success, or a human-readable error message upon
+// failure.
+// Neither `junction_name` nor `junction_target` needs to have a "\\?\" prefix,
+// not even if they are longer than MAX_PATH, though it's okay if they do. This
+// function will add the right prefixes as necessary.
+string CreateJunction(const wstring& junction_name,
+ const wstring& junction_target);
+
} // namespace windows_util
#endif // BAZEL_SRC_MAIN_NATIVE_WINDOWS_FILE_OPERATIONS_H_
diff --git a/src/test/cpp/util/BUILD b/src/test/cpp/util/BUILD
index c8b2f57caf..3e696ce828 100644
--- a/src/test/cpp/util/BUILD
+++ b/src/test/cpp/util/BUILD
@@ -82,20 +82,34 @@ cc_library(
"//src:windows_msvc": ["windows_test_util.cc"],
"//conditions:default": [],
}),
- deps = ["//third_party:gtest"],
+ hdrs = select({
+ "//src:windows": ["windows_test_util.h"],
+ "//src:windows_msvc": ["windows_test_util.h"],
+ "//conditions:default": [],
+ }),
+ visibility = [
+ "//src/test/cpp:__subpackages__",
+ "//src/test/native:__subpackages__",
+ ],
)
cc_test(
name = "windows_test_util_test",
size = "small",
srcs = select({
- "//src:windows": ["windows_test_util.cc"],
- "//src:windows_msvc": ["windows_test_util.cc"],
+ "//src:windows": ["windows_test_util_test.cc"],
+ "//src:windows_msvc": ["windows_test_util_test.cc"],
"//conditions:default": ["dummy_test.cc"],
}),
deps = select({
- "//src:windows": ["//third_party:gtest"],
- "//src:windows_msvc": ["//third_party:gtest"],
+ "//src:windows": [
+ ":windows_test_util",
+ "//third_party:gtest",
+ ],
+ "//src:windows_msvc": [
+ ":windows_test_util",
+ "//third_party:gtest",
+ ],
"//conditions:default": [],
}),
)
diff --git a/src/test/cpp/util/windows_test_util.cc b/src/test/cpp/util/windows_test_util.cc
index dfea7b05bb..47bdc31bbf 100644
--- a/src/test/cpp/util/windows_test_util.cc
+++ b/src/test/cpp/util/windows_test_util.cc
@@ -34,6 +34,9 @@ wstring GetTestTmpDirW() {
::GetEnvironmentVariableW(L"TEST_TMPDIR", buf.get(), size);
wstring result(buf.get());
std::replace(result.begin(), result.end(), '/', '\\');
+ if (result.back() == '\\') {
+ result.pop_back();
+ }
return result;
}
diff --git a/src/test/cpp/util/windows_test_util.h b/src/test/cpp/util/windows_test_util.h
index 2ec5844e41..ababc3e685 100644
--- a/src/test/cpp/util/windows_test_util.h
+++ b/src/test/cpp/util/windows_test_util.h
@@ -21,7 +21,8 @@ namespace blaze_util {
using std::wstring;
// Returns $TEST_TMPDIR as a wstring.
-// The result will have backslashes as directory separators (but no UNC prefix).
+// The result will have backslashes as directory separators, but no UNC prefix.
+// The result will also not have a trailing backslash.
wstring GetTestTmpDirW();
// Deletes all files and directories under `path`.
diff --git a/src/test/native/BUILD b/src/test/native/BUILD
index bd3f90d40b..be04c76466 100644
--- a/src/test/native/BUILD
+++ b/src/test/native/BUILD
@@ -9,19 +9,28 @@ filegroup(
)
cc_test(
- name = "windows_util_test",
+ name = "windows_jni_test",
+ size = "small",
srcs = select({
- "//src:windows": ["windows_util_test.cc"],
- "//src:windows_msvc": ["windows_util_test.cc"],
+ "//src:windows": [
+ "windows_util_test.cc",
+ "windows_file_operations_test.cc",
+ ],
+ "//src:windows_msvc": [
+ "windows_util_test.cc",
+ "windows_file_operations_test.cc",
+ ],
"//conditions:default": ["dummy_test.cc"],
}),
deps = select({
"//src:windows": [
"//src/main/native:windows_jni_lib",
+ "//src/test/cpp/util:windows_test_util",
"//third_party:gtest",
],
"//src:windows_msvc": [
"//src/main/native:windows_jni_lib",
+ "//src/test/cpp/util:windows_test_util",
"//third_party:gtest",
],
"//conditions:default": [],
diff --git a/src/test/native/windows_file_operations_test.cc b/src/test/native/windows_file_operations_test.cc
new file mode 100644
index 0000000000..f3258a9a86
--- /dev/null
+++ b/src/test/native/windows_file_operations_test.cc
@@ -0,0 +1,111 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#include <stdlib.h>
+#include <string.h>
+#include <windows.h>
+
+#include <memory> // unique_ptr
+#include <sstream>
+#include <string>
+
+#include "gtest/gtest.h"
+#include "src/main/native/windows_file_operations.h"
+#include "src/test/cpp/util/windows_test_util.h"
+
+#if !defined(COMPILER_MSVC) && !defined(__CYGWIN__)
+#error("This test should only be run on Windows")
+#endif // !defined(COMPILER_MSVC) && !defined(__CYGWIN__)
+
+namespace windows_util {
+
+using blaze_util::DeleteAllUnder;
+using blaze_util::GetTestTmpDirW;
+using std::string;
+using std::unique_ptr;
+using std::wstring;
+
+static const wstring kUncPrefix = wstring(L"\\\\?\\");
+
+class WindowsFileOperationsTest : public ::testing::Test {
+ public:
+ void TearDown() override { DeleteAllUnder(GetTestTmpDirW()); }
+};
+
+TEST_F(WindowsFileOperationsTest, TestCreateJunction) {
+ wstring tmp(kUncPrefix + GetTestTmpDirW());
+ wstring target(tmp + L"\\junc_target");
+ EXPECT_TRUE(::CreateDirectoryW(target.c_str(), NULL));
+ wstring file1(target + L"\\foo");
+ EXPECT_TRUE(blaze_util::CreateDummyFile(file1));
+
+ EXPECT_EQ(IS_JUNCTION_NO, IsJunctionOrDirectorySymlink(target.c_str()));
+ EXPECT_NE(INVALID_FILE_ATTRIBUTES, ::GetFileAttributesW(file1.c_str()));
+
+ wstring name(tmp + L"\\junc_name");
+
+ // Create junctions from all combinations of UNC-prefixed or non-prefixed name
+ // and target paths.
+ ASSERT_EQ("", CreateJunction(name + L"1", target));
+ ASSERT_EQ("", CreateJunction(name + L"2", target.substr(4)));
+ ASSERT_EQ("", CreateJunction(name.substr(4) + L"3", target));
+ ASSERT_EQ("", CreateJunction(name.substr(4) + L"4", target.substr(4)));
+
+ // Assert creation of the junctions.
+ ASSERT_EQ(IS_JUNCTION_YES,
+ IsJunctionOrDirectorySymlink((name + L"1").c_str()));
+ ASSERT_EQ(IS_JUNCTION_YES,
+ IsJunctionOrDirectorySymlink((name + L"2").c_str()));
+ ASSERT_EQ(IS_JUNCTION_YES,
+ IsJunctionOrDirectorySymlink((name + L"3").c_str()));
+ ASSERT_EQ(IS_JUNCTION_YES,
+ IsJunctionOrDirectorySymlink((name + L"4").c_str()));
+
+ // Assert that the file is visible under all junctions.
+ ASSERT_NE(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"1\\foo").c_str()));
+ ASSERT_NE(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"2\\foo").c_str()));
+ ASSERT_NE(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"3\\foo").c_str()));
+ ASSERT_NE(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"4\\foo").c_str()));
+
+ // Assert that no other file exists under the junctions.
+ wstring file2(target + L"\\bar");
+ ASSERT_EQ(INVALID_FILE_ATTRIBUTES, ::GetFileAttributesW(file2.c_str()));
+ ASSERT_EQ(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"1\\bar").c_str()));
+ ASSERT_EQ(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"2\\bar").c_str()));
+ ASSERT_EQ(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"3\\bar").c_str()));
+ ASSERT_EQ(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"4\\bar").c_str()));
+
+ // Create a new file.
+ EXPECT_TRUE(blaze_util::CreateDummyFile(file2));
+ EXPECT_NE(INVALID_FILE_ATTRIBUTES, ::GetFileAttributesW(file2.c_str()));
+
+ // Assert that the newly created file appears under all junctions.
+ ASSERT_NE(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"1\\bar").c_str()));
+ ASSERT_NE(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"2\\bar").c_str()));
+ ASSERT_NE(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"3\\bar").c_str()));
+ ASSERT_NE(INVALID_FILE_ATTRIBUTES,
+ ::GetFileAttributesW((name + L"4\\bar").c_str()));
+}
+
+} // namespace windows_util