| // 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 <algorithm> // replace |
| #include <functional> // function |
| #include <memory> // unique_ptr |
| #include <string> |
| |
| #include "gtest/gtest.h" |
| #include "src/main/native/windows/util.h" |
| |
| #if !defined(COMPILER_MSVC) && !defined(__CYGWIN__) |
| #error("This test should only be run on Windows") |
| #endif // !defined(COMPILER_MSVC) && !defined(__CYGWIN__) |
| |
| namespace bazel { |
| namespace windows { |
| |
| using std::wstring; |
| using std::unique_ptr; |
| using std::wstring; |
| |
| static const wstring kUncPrefix = wstring(L"\\\\?\\"); |
| |
| // Retrieves TEST_TMPDIR as a shortened path. Result won't have a "\\?\" prefix. |
| static void GetShortTempDir(wstring* result) { |
| unique_ptr<WCHAR[]> buf; |
| DWORD size = ::GetEnvironmentVariableW(L"TEST_TMPDIR", NULL, 0); |
| ASSERT_GT(size, (DWORD)0); |
| // `size` accounts for the null-terminator |
| buf.reset(new WCHAR[size]); |
| ::GetEnvironmentVariableW(L"TEST_TMPDIR", buf.get(), size); |
| |
| // Add "\\?\" prefix. |
| wstring tmpdir = kUncPrefix + wstring(buf.get()); |
| |
| // Remove trailing '/' or '\' and replace all '/' with '\'. |
| if (tmpdir.back() == '/' || tmpdir.back() == '\\') { |
| tmpdir.pop_back(); |
| } |
| std::replace(tmpdir.begin(), tmpdir.end(), '/', '\\'); |
| |
| // Convert to 8dot3 style short path. |
| size = ::GetShortPathNameW(tmpdir.c_str(), NULL, 0); |
| ASSERT_GT(size, (DWORD)0); |
| // `size` accounts for the null-terminator |
| buf.reset(new WCHAR[size]); |
| ::GetShortPathNameW(tmpdir.c_str(), buf.get(), size); |
| |
| // Set the result, omit the "\\?\" prefix. |
| // Ensure that the result is shorter than MAX_PATH and also has room for a |
| // backslash (1 wchar) and a single-letter executable name with .bat |
| // extension (5 wchars). |
| *result = wstring(buf.get() + 4); |
| ASSERT_LT(result->size(), MAX_PATH - 6); |
| } |
| |
| // If `success` is true, returns an empty string, otherwise an error message. |
| // The error message will have the format "Failed: operation(arg)" using the |
| // specified `operation` and `arg` strings. |
| static wstring ReturnEmptyOrError(bool success, const wstring& operation, |
| const wstring& arg) { |
| return success ? L"" : (wstring(L"Failed: ") + operation + L"(" + arg + L")"); |
| } |
| |
| // Creates a dummy file under `path`. `path` should NOT have a "\\?\" prefix. |
| static wstring CreateDummyFile(wstring path) { |
| path = kUncPrefix + path; |
| HANDLE handle = ::CreateFileW( |
| /* lpFileName */ path.c_str(), |
| /* dwDesiredAccess */ GENERIC_WRITE, |
| /* dwShareMode */ FILE_SHARE_READ, |
| /* lpSecurityAttributes */ NULL, |
| /* dwCreationDisposition */ CREATE_ALWAYS, |
| /* dwFlagsAndAttributes */ FILE_ATTRIBUTE_NORMAL, |
| /* hTemplateFile */ NULL); |
| if (handle == INVALID_HANDLE_VALUE) { |
| return ReturnEmptyOrError(false, L"CreateFileW", path); |
| } |
| DWORD actually_written = 0; |
| WriteFile(handle, "hello", 5, &actually_written, NULL); |
| if (actually_written == 0) { |
| return ReturnEmptyOrError(false, L"WriteFile", path); |
| } |
| CloseHandle(handle); |
| return L""; |
| } |
| |
| // Asserts that a dummy file under `path` can be created. |
| // This is a macro so the assertions will have the correct line number. |
| #define CREATE_FILE(/* const wstring& */ path) \ |
| { ASSERT_EQ(CreateDummyFile(path), L""); } |
| |
| // Deletes a file under `path`. `path` should NOT have a "\\?\" prefix. |
| static wstring DeleteDummyFile(wstring path) { |
| path = kUncPrefix + path; |
| return ReturnEmptyOrError(::DeleteFileW(path.c_str()), L"DeleteFileW", path); |
| } |
| |
| // Asserts that a file under `path` can be deleted. |
| // This is a macro so the assertions will have the correct line number. |
| #define DELETE_FILE(/* const wstring& */ path) \ |
| { ASSERT_EQ(DeleteDummyFile(path), L""); } |
| |
| // Creates a directory under `path`. `path` should NOT have a "\\?\" prefix. |
| static wstring CreateDir(wstring path) { |
| path = kUncPrefix + path; |
| return ReturnEmptyOrError(::CreateDirectoryW(path.c_str(), NULL), |
| L"CreateDirectoryW", path); |
| } |
| |
| // Asserts that a directory under `path` can be created. |
| // This is a macro so the assertions will have the correct line number. |
| #define CREATE_DIR(/* const wstring& */ path) \ |
| { ASSERT_EQ(CreateDir(path), L""); } |
| |
| // Deletes an empty directory under `path`. |
| // `path` should NOT have a "\\?\" prefix. |
| static wstring DeleteDir(wstring path) { |
| path = kUncPrefix + path; |
| return ReturnEmptyOrError(::RemoveDirectoryW(path.c_str()), |
| L"RemoveDirectoryW", path); |
| } |
| |
| // Asserts that the empty directory under `path` can be deleted. |
| // This is a macro so the assertions will have the correct line number. |
| #define DELETE_DIR(/* const wstring& */ path) \ |
| { ASSERT_EQ(DeleteDir(path), L""); } |
| |
| // Appends a file name segment with ".bat" extension to `result`. |
| // `length` specifies how long the segment may be, and it includes the "\" at |
| // the beginning. `length` must be in [6..13], so the shortest segment is |
| // "\a.bat", the longest is "\abcdefgh.bat". |
| // For example APPEND_FILE_SEGMENT(8, result) will append "\abc.bat" to |
| // `result`. |
| // This is a macro so the assertions will have the correct line number. |
| #define APPEND_FILE_SEGMENT(/* size_t */ length, /* wstring* */ result) \ |
| { \ |
| ASSERT_GE(length, 6); \ |
| ASSERT_LE(length, 13); \ |
| *result += wstring(L"\\abcdefgh", length - 4) + L".bat"; \ |
| } |
| |
| // Creates subdirectories under `basedir` and sets `result_path` to the deepest. |
| // |
| // `basedir` should be a shortened path, without "\\?\" prefix. |
| // `result_path` will be also a short path under `basedir`. |
| // |
| // Every directory in `result_path` will be created. The entire length of this |
| // path will be exactly MAX_PATH - 7 (not including null-terminator). |
| // Just by appending a file name segment between 6 and 8 characters long (i.e. |
| // "\a.bat", "\ab.bat", or "\abc.bat") the caller can obtain a path that is |
| // MAX_PATH - 1 long, or MAX_PATH long, or MAX_PATH + 1 long, respectively, |
| // and cannot be shortened further. |
| static void CreateShortDirsUnder(wstring basedir, wstring* result_path) { |
| ASSERT_LT(basedir.size(), MAX_PATH); |
| size_t remaining_len = MAX_PATH - 1 - basedir.size(); |
| ASSERT_GE(remaining_len, 6); // has room for suffix "\a.bat" |
| |
| // If `remaining_len` is odd, make it even. |
| if (remaining_len % 2) { |
| remaining_len -= 3; |
| basedir += wstring(L"\\ab"); |
| CREATE_DIR(basedir); |
| } |
| |
| // Keep adding 2 chars long segments until we only have 6 chars left. |
| while (remaining_len >= 8) { |
| remaining_len -= 2; |
| basedir += wstring(L"\\a"); |
| CREATE_DIR(basedir); |
| } |
| ASSERT_EQ(basedir.size(), MAX_PATH - 1 - 6); |
| *result_path = basedir; |
| } |
| |
| // Deletes `deepest_subdir` and all of its parents below `basedir`. |
| // `basedir` must be a prefix (ancestor) of `deepest_subdir`. Neither of them |
| // should have a "\\?\" prefix. |
| // Every subdirectory connecting `basedir` and `deepest_subdir` must be empty |
| // except for the single directory child connecting these two nodes. In other |
| // words it should be possible to remove all directories starting at |
| // `deepest_subdir` and walking up the tree until `basedir` is reached. |
| // `basedir` is NOT deleted and it doesn't need to be empty either. |
| static void DeleteDirsUnder(const wstring& basedir, |
| const wstring& deepest_subdir) { |
| // Assert that `deepest_subdir` starts with `basedir`. |
| ASSERT_EQ(deepest_subdir.find(basedir), 0); |
| |
| // Make a mutable copy of `deepest_subdir`. |
| unique_ptr<WCHAR[]> mutable_path(new WCHAR[deepest_subdir.size() + 1]); |
| memcpy(mutable_path.get(), deepest_subdir.c_str(), |
| deepest_subdir.size() * sizeof(WCHAR)); |
| mutable_path.get()[deepest_subdir.size()] = 0; |
| |
| // Mark the end of the path. We'll keep setting the last directory separator |
| // to the null-terminator, thus walking up the directory tree. |
| WCHAR* p_end = mutable_path.get() + deepest_subdir.size(); |
| |
| while (p_end > mutable_path.get() + basedir.size()) { |
| DELETE_DIR(mutable_path.get()); |
| // Walk up one level in the path. |
| while (*p_end != '\\') { |
| --p_end; |
| } |
| *p_end = '\0'; |
| } |
| } |
| |
| // Asserts that `str` contains substring `substr`. |
| // This is a macro so the assertions will have the correct line number. |
| #define ASSERT_CONTAINS(/* const wstring& */ str, /* const WCHAR* */ substr) \ |
| { \ |
| ASSERT_NE(str, L""); \ |
| ASSERT_NE(str.find(substr), wstring::npos); \ |
| } |
| |
| // This is a macro so the assertions will have the correct line number. |
| #define ASSERT_SHORTENING_FAILS(/* const WCHAR* */ input, \ |
| /* const WCHAR* */ error_msg) \ |
| { \ |
| wstring actual; \ |
| wstring result(AsExecutablePathForCreateProcess(input, &actual)); \ |
| ASSERT_CONTAINS(result, error_msg); \ |
| } |
| |
| // This is a macro so the assertions will have the correct line number. |
| #define ASSERT_SHORTENING_SUCCEEDS(/* const WCHAR* */ input, \ |
| /* const wstring& */ expected_result) \ |
| { \ |
| wstring actual; \ |
| ASSERT_EQ(AsExecutablePathForCreateProcess(input, &actual), L""); \ |
| ASSERT_EQ(actual, expected_result); \ |
| } |
| |
| TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessBadInputs) { |
| ASSERT_SHORTENING_FAILS(L"", L"should not be empty"); |
| ASSERT_SHORTENING_FAILS(L"\"cmd.exe\"", L"path should not be quoted"); |
| ASSERT_SHORTENING_FAILS(L"/dev/null", L"path is absolute without a drive"); |
| ASSERT_SHORTENING_FAILS(L"/usr/bin/bash", L"path is absolute without a"); |
| ASSERT_SHORTENING_FAILS(L"foo\\bar.exe", L"path is not absolute"); |
| ASSERT_SHORTENING_FAILS(L"foo\\..\\bar.exe", L"path is not normalized"); |
| ASSERT_SHORTENING_FAILS(L"\\bar.exe", L"path is absolute"); |
| |
| wstring dummy = L"hello"; |
| while (dummy.size() < MAX_PATH) { |
| dummy += dummy; |
| } |
| dummy += L".exe"; |
| ASSERT_SHORTENING_FAILS(dummy.c_str(), L"a file name but too long"); |
| } |
| |
| TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessConversions) { |
| wstring tmpdir; |
| GetShortTempDir(&tmpdir); |
| wstring short_root; |
| CreateShortDirsUnder(tmpdir, &short_root); |
| |
| // Assert that we have enough room to append a file name that is just short |
| // enough to fit into MAX_PATH - 1, or one that's just long enough to make |
| // the whole path MAX_PATH long or longer. |
| ASSERT_EQ(short_root.size(), MAX_PATH - 1 - 6); |
| |
| wstring actual; |
| wstring error; |
| for (size_t i = 0; i < 3; ++i) { |
| wstring wfilename = short_root; |
| |
| APPEND_FILE_SEGMENT(6 + i, &wfilename); |
| ASSERT_EQ(wfilename.size(), MAX_PATH - 1 + i); |
| |
| // When i=0 then `wfilename` is MAX_PATH - 1 long, so |
| // `AsExecutablePathForCreateProcess` will not attempt to shorten it, and |
| // so it also won't notice that the file doesn't exist. If however we pass |
| // a non-existent path to CreateProcessA, then it'll fail, so we'll find out |
| // about this error in production code. |
| // When i>0 then `wfilename` is at least MAX_PATH long, so |
| // `AsExecutablePathForCreateProcess` will attempt to shorten it, but |
| // because the file doesn't yet exist, the shortening attempt will fail. |
| if (i > 0) { |
| ASSERT_EQ(::GetFileAttributesW(wfilename.c_str()), |
| INVALID_FILE_ATTRIBUTES); |
| ASSERT_SHORTENING_FAILS(wfilename.c_str(), L"GetShortPathNameW"); |
| } |
| |
| // Create the file, now we should be able to shorten it when i=0, but not |
| // otherwise. |
| CREATE_FILE(wfilename); |
| if (i == 0) { |
| // The wfilename was short enough. |
| ASSERT_SHORTENING_SUCCEEDS(wfilename.c_str(), |
| wstring(L"\"") + wfilename + L"\""); |
| } else { |
| // The wfilename was too long to begin with, and it was impossible to |
| // shorten any of the segments (since we deliberately created them that |
| // way), so shortening failed. |
| ASSERT_SHORTENING_FAILS(wfilename.c_str(), L"cannot shorten the path"); |
| } |
| DELETE_FILE(wfilename); |
| } |
| |
| // Finally construct a path that can and will be shortened. Just walk up a few |
| // levels in `short_root` and create a long file name that can be shortened. |
| wstring wshortenable_root = short_root; |
| while (wshortenable_root.size() > MAX_PATH - 1 - 13) { |
| wshortenable_root = |
| wshortenable_root.substr(0, wshortenable_root.find_last_of(L'\\')); |
| } |
| wstring wshortenable = wshortenable_root + wstring(L"\\") + |
| wstring(MAX_PATH - wshortenable_root.size(), L'a') + |
| wstring(L".bat"); |
| ASSERT_GT(wshortenable.size(), MAX_PATH); |
| |
| // Attempt to shorten. It will fail because the file doesn't exist yet. |
| ASSERT_SHORTENING_FAILS(wshortenable, L"GetShortPathNameW"); |
| |
| // Create the file so shortening will succeed. |
| CREATE_FILE(wshortenable); |
| ASSERT_SHORTENING_SUCCEEDS( |
| wshortenable, wstring(L"\"") + wshortenable_root + L"\\AAAAAA~1.BAT\""); |
| DELETE_FILE(wshortenable); |
| |
| DeleteDirsUnder(tmpdir, short_root); |
| } |
| |
| } // namespace windows |
| } // namespace bazel |