diff options
author | 2017-02-15 13:07:52 +0000 | |
---|---|---|
committer | 2017-02-15 14:47:42 +0000 | |
commit | d7d4def945b38133c43e3df3ea7c450b229d2bfb (patch) | |
tree | b370d2c91c525a692e4eee01e2fbe6331e7e1df2 | |
parent | 2a0bb37397e1354eaf06b7697913f71443332175 (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
-rw-r--r-- | src/main/cpp/util/file_windows.cc | 7 | ||||
-rw-r--r-- | src/main/native/windows_file_operations.cc | 121 | ||||
-rw-r--r-- | src/main/native/windows_file_operations.h | 18 | ||||
-rw-r--r-- | src/test/cpp/util/BUILD | 24 | ||||
-rw-r--r-- | src/test/cpp/util/windows_test_util.cc | 3 | ||||
-rw-r--r-- | src/test/cpp/util/windows_test_util.h | 3 | ||||
-rw-r--r-- | src/test/native/BUILD | 15 | ||||
-rw-r--r-- | src/test/native/windows_file_operations_test.cc | 111 |
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 |