diff options
Diffstat (limited to 'src/main/cpp/util/path_windows.cc')
-rw-r--r-- | src/main/cpp/util/path_windows.cc | 420 |
1 files changed, 420 insertions, 0 deletions
diff --git a/src/main/cpp/util/path_windows.cc b/src/main/cpp/util/path_windows.cc new file mode 100644 index 0000000000..d3756c71bf --- /dev/null +++ b/src/main/cpp/util/path_windows.cc @@ -0,0 +1,420 @@ +// Copyright 2018 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 "src/main/cpp/util/path_platform.h" + +#include <wchar.h> // wcslen +#include <windows.h> + +#include <algorithm> +#include <memory> // unique_ptr +#include <sstream> +#include <vector> + +#include "src/main/cpp/util/errors.h" +#include "src/main/cpp/util/exit_code.h" +#include "src/main/cpp/util/file_platform.h" +#include "src/main/cpp/util/logging.h" +#include "src/main/cpp/util/strings.h" +#include "src/main/native/windows/file.h" + +namespace blaze_util { + +using bazel::windows::HasUncPrefix; + +static char GetCurrentDrive(); + +template <typename char_type> +struct CharTraits { + static bool IsAlpha(char_type ch); +}; + +template <> +struct CharTraits<char> { + static bool IsAlpha(char ch) { return isalpha(ch); } +}; + +template <> +struct CharTraits<wchar_t> { + static bool IsAlpha(wchar_t ch) { return iswalpha(ch); } +}; + +template <typename char_type> +static bool IsPathSeparator(char_type ch) { + return ch == '/' || ch == '\\'; +} + +template <typename char_type> +static bool HasDriveSpecifierPrefix(const char_type* ch) { + return CharTraits<char_type>::IsAlpha(ch[0]) && ch[1] == ':'; +} + +std::string ConvertPath(const std::string& path) { + // The path may not be Windows-style and may not be normalized, so convert it. + std::wstring wpath; + std::string error; + if (!AsAbsoluteWindowsPath(path, &wpath, &error)) { + BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR) + << "ConvertPath(" << path + << "): AsAbsoluteWindowsPath failed: " << error; + } + std::transform(wpath.begin(), wpath.end(), wpath.begin(), ::towlower); + return std::string( + WstringToCstring(RemoveUncPrefixMaybe(wpath.c_str())).get()); +} + +bool CompareAbsolutePaths(const std::string& a, const std::string& b) { + return ConvertPath(a) == ConvertPath(b); +} + +std::string PathAsJvmFlag(const std::string& path) { + std::string spath; + std::string error; + if (!AsShortWindowsPath(path, &spath, &error)) { + BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR) + << "PathAsJvmFlag(" << path + << "): AsShortWindowsPath failed: " << error; + } + // Convert backslashes to forward slashes, in order to avoid the JVM parsing + // Windows paths as if they contained escaped characters. + // See https://github.com/bazelbuild/bazel/issues/2576 + std::replace(spath.begin(), spath.end(), '\\', '/'); + return spath; +} + +void AddUncPrefixMaybe(std::wstring* path) { + if (path->size() >= MAX_PATH && !HasUncPrefix(path->c_str())) { + *path = std::wstring(L"\\\\?\\") + *path; + } +} + +const wchar_t* RemoveUncPrefixMaybe(const wchar_t* ptr) { + return ptr + (HasUncPrefix(ptr) ? 4 : 0); +} + +// Checks if the path is absolute and/or is a root path. +// +// If `must_be_root` is true, then in addition to being absolute, the path must +// also be just the root part, no other components, e.g. "c:\" is both absolute +// and root, but "c:\foo" is just absolute. +template <typename char_type> +static bool IsRootOrAbsolute(const std::basic_string<char_type>& path, + bool must_be_root) { + // An absolute path is one that starts with "/", "\", "c:/", "c:\", + // "\\?\c:\", or rarely "\??\c:\" or "\\.\c:\". + // + // It is unclear whether the UNC prefix is just "\\?\" or is "\??\" also + // valid (in some cases it seems to be, though MSDN doesn't mention it). + return + // path is (or starts with) "/" or "\" + ((must_be_root ? path.size() == 1 : !path.empty()) && + IsPathSeparator(path[0])) || + // path is (or starts with) "c:/" or "c:\" or similar + ((must_be_root ? path.size() == 3 : path.size() >= 3) && + HasDriveSpecifierPrefix(path.c_str()) && IsPathSeparator(path[2])) || + // path is (or starts with) "\\?\c:\" or "\??\c:\" or similar + ((must_be_root ? path.size() == 7 : path.size() >= 7) && + HasUncPrefix(path.c_str()) && + HasDriveSpecifierPrefix(path.c_str() + 4) && IsPathSeparator(path[6])); +} + +template <typename char_type> +static std::pair<std::basic_string<char_type>, std::basic_string<char_type> > +SplitPathImpl(const std::basic_string<char_type>& path) { + if (path.empty()) { + return std::make_pair(std::basic_string<char_type>(), + std::basic_string<char_type>()); + } + + size_t pos = path.size() - 1; + for (auto it = path.crbegin(); it != path.crend(); ++it, --pos) { + if (IsPathSeparator(*it)) { + if ((pos == 2 || pos == 6) && + IsRootOrAbsolute(path.substr(0, pos + 1), /* must_be_root */ true)) { + // Windows path, top-level directory, e.g. "c:\foo", + // result is ("c:\", "foo"). + // Or UNC path, top-level directory, e.g. "\\?\c:\foo" + // result is ("\\?\c:\", "foo"). + return std::make_pair( + // Include the "/" or "\" in the drive specifier. + path.substr(0, pos + 1), path.substr(pos + 1)); + } else { + // Windows path (neither top-level nor drive root), Unix path, or + // relative path. + return std::make_pair( + // If the only "/" is the leading one, then that shall be the first + // pair element, otherwise the substring up to the rightmost "/". + pos == 0 ? path.substr(0, 1) : path.substr(0, pos), + // If the rightmost "/" is the tail, then the second pair element + // should be empty. + pos == path.size() - 1 ? std::basic_string<char_type>() + : path.substr(pos + 1)); + } + } + } + // Handle the case with no '/' or '\' in `path`. + return std::make_pair(std::basic_string<char_type>(), path); +} + +std::pair<std::string, std::string> SplitPath(const std::string& path) { + return SplitPathImpl(path); +} + +std::pair<std::wstring, std::wstring> SplitPathW(const std::wstring& path) { + return SplitPathImpl(path); +} + +bool AsWindowsPath(const std::string& path, std::string* result, + std::string* error) { + if (path.empty()) { + result->clear(); + return true; + } + if (IsDevNull(path.c_str())) { + result->assign("NUL"); + return true; + } + if (HasUncPrefix(path.c_str())) { + // Path has "\\?\" prefix --> assume it's already Windows-style. + *result = path.c_str(); + return true; + } + if (IsPathSeparator(path[0]) && path.size() > 1 && IsPathSeparator(path[1])) { + // Unsupported path: "\\" or "\\server\path", or some degenerate form of + // these, such as "//foo". + if (error) { + *error = "network paths are unsupported"; + } + return false; + } + if (HasDriveSpecifierPrefix(path.c_str()) && + (path.size() < 3 || !IsPathSeparator(path[2]))) { + // Unsupported path: "c:" or "c:foo" + if (error) { + *error = "working-directory relative paths are unsupported"; + } + return false; + } + + std::string mutable_path = path; + if (path[0] == '/') { + if (error) { + *error = "Unix-style paths are unsupported"; + } + return false; + } + + if (path[0] == '\\') { + // This is an absolute Windows path on the current drive, e.g. "\foo\bar". + mutable_path = std::string(1, GetCurrentDrive()) + ":" + path; + } // otherwise this is a relative path, or absolute Windows path. + + result->assign(NormalizeWindowsPath(mutable_path)); + return true; +} + +bool AsWindowsPath(const std::string& path, std::wstring* result, + std::string* error) { + std::string normalized_win_path; + if (!AsWindowsPath(path, &normalized_win_path, error)) { + return false; + } + + result->assign(CstringToWstring(normalized_win_path.c_str()).get()); + return true; +} + +bool AsAbsoluteWindowsPath(const std::string& path, std::wstring* result, + std::string* error) { + if (path.empty()) { + result->clear(); + return true; + } + if (IsDevNull(path.c_str())) { + result->assign(L"NUL"); + return true; + } + if (!AsWindowsPath(path, result, error)) { + return false; + } + if (!IsRootOrAbsolute(*result, /* must_be_root */ false)) { + *result = GetCwdW() + L"\\" + *result; + } + if (!HasUncPrefix(result->c_str())) { + *result = std::wstring(L"\\\\?\\") + *result; + } + return true; +} + +bool AsShortWindowsPath(const std::string& path, std::string* result, + std::string* error) { + if (IsDevNull(path.c_str())) { + result->assign("NUL"); + return true; + } + + result->clear(); + std::wstring wpath; + std::wstring wsuffix; + if (!AsAbsoluteWindowsPath(path, &wpath, error)) { + return false; + } + DWORD size = ::GetShortPathNameW(wpath.c_str(), nullptr, 0); + if (size == 0) { + // GetShortPathNameW can fail if `wpath` does not exist. This is expected + // when we are about to create a file at that path, so instead of failing, + // walk up in the path until we find a prefix that exists and can be + // shortened, or is a root directory. Save the non-existent tail in + // `wsuffix`, we'll add it back later. + std::vector<std::wstring> segments; + while (size == 0 && !IsRootDirectoryW(wpath)) { + std::pair<std::wstring, std::wstring> split = SplitPathW(wpath); + wpath = split.first; + segments.push_back(split.second); + size = ::GetShortPathNameW(wpath.c_str(), nullptr, 0); + } + + // Join all segments. + std::wostringstream builder; + bool first = true; + for (auto it = segments.crbegin(); it != segments.crend(); ++it) { + if (!first || !IsRootDirectoryW(wpath)) { + builder << L'\\' << *it; + } else { + builder << *it; + } + first = false; + } + wsuffix = builder.str(); + } + + std::wstring wresult; + if (IsRootDirectoryW(wpath)) { + // Strip the UNC prefix from `wpath`, and the leading "\" from `wsuffix`. + wresult = std::wstring(RemoveUncPrefixMaybe(wpath.c_str())) + wsuffix; + } else { + std::unique_ptr<WCHAR[]> wshort( + new WCHAR[size]); // size includes null-terminator + if (size - 1 != ::GetShortPathNameW(wpath.c_str(), wshort.get(), size)) { + if (error) { + std::string last_error = GetLastErrorString(); + std::stringstream msg; + msg << "AsShortWindowsPath(" << path << "): GetShortPathNameW(" + << WstringToString(wpath) << ") failed: " << last_error; + *error = msg.str(); + } + return false; + } + // GetShortPathNameW may preserve the UNC prefix in the result, so strip it. + wresult = std::wstring(RemoveUncPrefixMaybe(wshort.get())) + wsuffix; + } + + result->assign(WstringToCstring(wresult.c_str()).get()); + ToLower(result); + return true; +} + +bool IsDevNull(const char* path) { + return path != NULL && *path != 0 && + (strncmp("/dev/null\0", path, 10) == 0 || + ((path[0] == 'N' || path[0] == 'n') && + (path[1] == 'U' || path[1] == 'u') && + (path[2] == 'L' || path[2] == 'l') && path[3] == 0)); +} + +bool IsRootDirectory(const std::string& path) { + return IsRootOrAbsolute(path, true); +} + +bool IsAbsolute(const std::string& path) { + return IsRootOrAbsolute(path, false); +} + +bool IsRootDirectoryW(const std::wstring& path) { + return IsRootOrAbsolute(path, true); +} + +static char GetCurrentDrive() { + std::wstring cwd = GetCwdW(); + wchar_t wdrive = RemoveUncPrefixMaybe(cwd.c_str())[0]; + wchar_t offset = wdrive >= L'A' && wdrive <= L'Z' ? L'A' : L'a'; + return 'a' + wdrive - offset; +} + +std::string NormalizeWindowsPath(std::string path) { + if (path.empty()) { + return ""; + } + if (path[0] == '/') { + // This is an absolute MSYS path, error out. + BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR) + << "NormalizeWindowsPath(" << path << "): expected a Windows path"; + } + if (path.size() >= 4 && HasUncPrefix(path.c_str())) { + path = path.substr(4); + } + + static const std::string dot("."); + static const std::string dotdot(".."); + + std::vector<std::string> segments; + int segment_start = -1; + // Find the path segments in `path` (separated by "/"). + for (int i = 0;; ++i) { + if (!IsPathSeparator(path[i]) && path[i] != '\0') { + // The current character does not end a segment, so start one unless it's + // already started. + if (segment_start < 0) { + segment_start = i; + } + } else if (segment_start >= 0 && i > segment_start) { + // The current character is "/" or "\0", so this ends a segment. + // Add that to `segments` if there's anything to add; handle "." and "..". + std::string segment(path, segment_start, i - segment_start); + segment_start = -1; + if (segment == dotdot) { + if (!segments.empty() && + !HasDriveSpecifierPrefix(segments[0].c_str())) { + segments.pop_back(); + } + } else if (segment != dot) { + segments.push_back(segment); + } + } + if (path[i] == '\0') { + break; + } + } + + // Handle the case when `path` is just a drive specifier (or some degenerate + // form of it, e.g. "c:\.."). + if (segments.size() == 1 && segments[0].size() == 2 && + HasDriveSpecifierPrefix(segments[0].c_str())) { + return segments[0] + '\\'; + } + + // Join all segments. + bool first = true; + std::ostringstream result; + for (const auto& s : segments) { + if (!first) { + result << '\\'; + } + first = false; + result << s; + } + return result.str(); +} + +} // namespace blaze_util |