From c2b332b45e6ea41a14ecbd3c5f30782bcdeec301 Mon Sep 17 00:00:00 2001 From: Laszlo Csomor Date: Thu, 22 Feb 2018 04:27:37 -0800 Subject: windows: replace custom JunctionResolver There's a realpath(3)-like Windows API function called GetFinalPathNameByHandle{A,W}. This commit removes JunctionResolver, implements RealPath using GetFinalPathNameByHandleW, and replaces JunctionResolver usages with RealPath. Change-Id: I69efd29c139e957a93496cd75a2db351ed4453e1 PiperOrigin-RevId: 186598195 --- src/main/cpp/util/file_windows.cc | 271 ++++++++------------------------------ 1 file changed, 57 insertions(+), 214 deletions(-) diff --git a/src/main/cpp/util/file_windows.cc b/src/main/cpp/util/file_windows.cc index fd63214574..e39faff7ff 100644 --- a/src/main/cpp/util/file_windows.cc +++ b/src/main/cpp/util/file_windows.cc @@ -29,22 +29,27 @@ namespace blaze_util { +using bazel::windows::AutoHandle; +using bazel::windows::GetLongPath; +using bazel::windows::HasUncPrefix; +using bazel::windows::OpenDirectory; using std::basic_string; using std::pair; using std::string; using std::unique_ptr; using std::wstring; -using bazel::windows::AutoHandle; -using bazel::windows::GetLongPath; -using bazel::windows::HasUncPrefix; -using bazel::windows::OpenDirectory; // Returns the current working directory as a Windows path. // The result may have a UNC prefix. static unique_ptr GetCwdW(); +static unique_ptr RealPath(const wstring& path, + DWORD* opt_attr = nullptr); + static char GetCurrentDrive(); +static bool IsDirectoryByAttributes(DWORD attrs); + // Returns true if `path` refers to a directory or (non-dangling) junction. // `path` must be a normalized Windows path, with UNC prefix (and absolute) if // necessary. @@ -123,8 +128,8 @@ class WindowsPipe : public IPipe { bool Send(const void* buffer, int size) override { DWORD actually_written = 0; - return ::WriteFile(_write_handle, buffer, size, &actually_written, - NULL) == TRUE; + return ::WriteFile(_write_handle, buffer, size, &actually_written, NULL) == + TRUE; } int Receive(void* buffer, int size, int* error) override { @@ -606,6 +611,12 @@ bool AsShortWindowsPath(const string& path, string* result) { return true; } +static HANDLE OpenFileForStat(const WCHAR* path) { + return CreateFileW(path, 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); +} + static bool OpenFileForReading(const string& filename, HANDLE* result) { if (filename.empty()) { return false; @@ -784,128 +795,6 @@ bool UnlinkPath(const string& file_path) { return UnlinkPathW(wpath); } -class JunctionResolver { - public: - JunctionResolver(); - - // Resolves junctions, or simply checks file existence (if not a junction). - // - // Returns true if `path` is not a junction and it exists. - // Returns true if `path` is a junction and can be successfully resolved and - // its target exists. - // Returns false otherwise. - // - // If `result` is not nullptr and the method returned false, then this will be - // reset to point to a new WCHAR buffer containing the final resolved path. - // If `path` was a junction, this will be the fully resolved path, otherwise - // it will be a copy of `path`. - bool Resolve(const WCHAR* path, std::unique_ptr* result); - - private: - static const int kMaximumJunctionDepth; - - // This struct is a simplified version of REPARSE_DATA_BUFFER, defined by - // the header file, which is not available on some systems. - // This struct removes the original one's union keeping only - // MountPointReparseBuffer, while also renames some fields to reflect how - // ::DeviceIoControl actually uses them when reading junction data. - typedef struct _ReparseMountPointData { - static const int kSize = MAXIMUM_REPARSE_DATA_BUFFER_SIZE; - - ULONG ReparseTag; - USHORT Dummy1; - USHORT Dummy2; - USHORT Dummy3; - USHORT Dummy4; - // Length of string in PathBuffer, in WCHARs, including the "\??\" prefix - // and the null-terminator. - // - // Reparse points use the "\??\" prefix instead of "\\?\", presumably - // because the junction is resolved by the kernel and it points to a Device - // Object path (which is what the kernel understands), and "\??" is a device - // path. ("\??" is shorthand for "\DosDevices" under which disk drives - // reside, e.g. "C:" is a symlink to "\DosDevices\C:" aka "\??\C:"). - // See (on 2017-01-04): - // https://msdn.microsoft.com/en-us/library/windows/hardware/ff565384(v=vs.85).aspx - // https://msdn.microsoft.com/en-us/library/windows/hardware/ff557762(v=vs.85).aspx - USHORT Size; - USHORT Dummy5; - // First character of the string returned by ::DeviceIoControl. The rest of - // the string follows this in memory, that's why the caller must allocate - // kSize bytes and cast that data to ReparseMountPointData. - WCHAR PathBuffer[1]; - } ReparseMountPointData; - - uint8_t reparse_buffer_bytes_[ReparseMountPointData::kSize]; - ReparseMountPointData* reparse_buffer_; - - bool Resolve(const WCHAR* path, std::unique_ptr* result, - int max_junction_depth); -}; - -// Maximum reparse point depth on Windows 8 and above is 63. -// Source (on 2016-12-20): -// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365503(v=vs.85).aspx -const int JunctionResolver::kMaximumJunctionDepth = 63; - -JunctionResolver::JunctionResolver() - : reparse_buffer_( - reinterpret_cast(reparse_buffer_bytes_)) { - reparse_buffer_->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; -} - -bool JunctionResolver::Resolve(const WCHAR* path, unique_ptr* result, - int max_junction_depth) { - DWORD attributes = ::GetFileAttributesW(path); - if (attributes == INVALID_FILE_ATTRIBUTES) { - // `path` does not exist. - return false; - } else { - if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && - (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { - // `path` is a junction. GetFileAttributesW succeeds for these even if - // their target does not exist. We need to resolve the target and check if - // that exists. (There seems to be no API function for this.) - if (max_junction_depth <= 0) { - // Too many levels of junctions. Simply say this file doesn't exist. - return false; - } - // Get a handle to the directory. - AutoHandle handle(OpenDirectory(path, /* read_write */ false)); - if (!handle.IsValid()) { - // Opening the junction failed for whatever reason. For all intents and - // purposes we can treat this file as if it didn't exist. - return false; - } - // Read out the junction data. - DWORD bytes_returned; - BOOL ok = ::DeviceIoControl( - handle, FSCTL_GET_REPARSE_POINT, NULL, 0, reparse_buffer_, - MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &bytes_returned, NULL); - if (!ok) { - // Reading the junction data failed. For all intents and purposes we can - // treat this file as if it didn't exist. - return false; - } - reparse_buffer_->PathBuffer[reparse_buffer_->Size - 1] = UNICODE_NULL; - // Check if the junction target exists. - return Resolve(reparse_buffer_->PathBuffer, result, - max_junction_depth - 1); - } - } - // `path` is a normal file or directory. - if (result) { - size_t len = wcslen(path) + 1; - result->reset(new WCHAR[len]); - memcpy(result->get(), path, len * sizeof(WCHAR)); - } - return true; -} - -bool JunctionResolver::Resolve(const WCHAR* path, unique_ptr* result) { - return Resolve(path, result, kMaximumJunctionDepth); -} - bool ReadDirectorySymlink(const string& name, string* result) { wstring wname; if (!AsAbsoluteWindowsPath(name, &wname)) { @@ -913,11 +802,11 @@ bool ReadDirectorySymlink(const string& name, string* result) { "ReadDirectorySymlink(%s): AsAbsoluteWindowsPath", name.c_str()); return false; } - unique_ptr result_ptr; - if (!JunctionResolver().Resolve(wname.c_str(), &result_ptr)) { + unique_ptr realpath(RealPath(wname)); + if (realpath == nullptr) { return false; } - *result = WstringToCstring(RemoveUncPrefixMaybe(result_ptr.get())).get(); + *result = WstringToCstring(RemoveUncPrefixMaybe(realpath.get())).get(); return true; } @@ -934,7 +823,29 @@ bool PathExists(const string& path) { "PathExists(%s): AsAbsoluteWindowsPath", path.c_str()); return false; } - return JunctionResolver().Resolve(wpath.c_str(), nullptr); + return AutoHandle(OpenFileForStat(wpath.c_str())).IsValid(); +} + +static unique_ptr RealPath(const wstring& path, DWORD* opt_attr) { + AutoHandle handle(OpenFileForStat(path.c_str())); + if (!handle.IsValid()) { + return std::move(unique_ptr()); + } + unique_ptr result(new WCHAR[path.size() + 1]); + DWORD size = GetFinalPathNameByHandleW(handle, result.get(), path.size(), 0); + if (size > path.size()) { + result.reset(new WCHAR[size + 1]); + GetFinalPathNameByHandleW(handle, result.get(), size, 0); + } + if (opt_attr != nullptr) { + BY_HANDLE_FILE_INFORMATION info; + if (GetFileInformationByHandle(handle, &info)) { + *opt_attr = info.dwFileAttributes; + } else { + *opt_attr = INVALID_FILE_ATTRIBUTES; + } + } + return std::move(result); } string MakeCanonical(const char* path) { @@ -949,84 +860,11 @@ string MakeCanonical(const char* path) { pdie(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR, "MakeCanonical(%s): AsAbsoluteWindowsPath", path); } - - // Resolve all segments of the path. Do this from leaf to root, so we always - // know that the path's tail is resolved and junctions may be found only in - // its head. - std::vector realpath_reversed; - while (true) { - // Resolve the last segment. - unique_ptr realpath; - if (!JunctionResolver().Resolve(wpath.c_str(), &realpath)) { - // The path doesn't exist or there are too many levels of indirection, - // so just give up. - return ""; - } - // The last segment is surely not a junction anymore. Split it off the path - // and keep resolving its ancestors until we reach the root directory. - pair split(SplitPathW(realpath.get())); - if (split.second.empty()) { - // `wpath` was a root directory, we're done. - realpath_reversed.push_back(split.first); - break; - } else { - // `wpath` was not yet a root directory, split off the last segment and - // store it in the stack, keep resolving the rest. - realpath_reversed.push_back(split.second); - wpath = split.first; - } - } - - // Concatenate the segments in reverse order. - int segment_cnt = 0; - std::wstringstream builder; - for (auto segment = realpath_reversed.crbegin(); - segment != realpath_reversed.crend(); ++segment) { - if (segment_cnt < 2) { - segment_cnt++; - } else { - // Start appending '\' separator after not the first but the second - // segment, since the first segment is a drive name "c:\" and already - // has the separator. - builder << L"\\"; - } - builder << *segment; - } - wstring realpath(builder.str()); - if (HasUncPrefix(realpath.c_str())) { - // `realpath` has an UNC prefix if `path` did, or if `path` contained - // junctions. - // In the first case, the UNC prefix is the usual "\\?\", but in the second - // case it is "\??\", because that's what the junction resolution yields, - // because that's the prefix the filesystem uses for storing junction - // values. - // Since "\??\" is only meaningful for the kernel and not for usermode - // Win32 API functions, we need to replace this prefix with the usual "\\?\" - // one. - realpath[1] = L'\\'; - } - - // Resolve all 8dot3 style segments of the path, if any. The input path may - // have had some. Junctions may also refer to 8dot3 names. - unique_ptr long_realpath; - wstring error(GetLongPath(realpath.c_str(), &long_realpath)); - if (!error.empty()) { - // TODO(laszlocsomor): refactor MakeCanonical to return an error message, - // return `error` here. + unique_ptr realpath(RealPath(wpath)); + if (realpath == nullptr) { return ""; } - - // Convert the path to lower-case. - size_t size = - wcslen(long_realpath.get()) - (HasUncPrefix(long_realpath.get()) ? 4 : 0); - unique_ptr lcase_realpath(new WCHAR[size + 1]); - const WCHAR* p_from = RemoveUncPrefixMaybe(long_realpath.get()); - WCHAR* p_to = lcase_realpath.get(); - while (size-- > 0) { - *p_to++ = towlower(*p_from++); - } - *p_to = 0; - return string(WstringToCstring(lcase_realpath.get()).get()); + return string(WstringToCstring(RemoveUncPrefixMaybe(realpath.get())).get()); } static bool CanReadFileW(const wstring& path) { @@ -1080,7 +918,7 @@ bool CanAccessDirectory(const std::string& path) { return false; } DWORD attr = ::GetFileAttributesW(wpath.c_str()); - if ((attr == INVALID_FILE_ATTRIBUTES) || !(attr & FILE_ATTRIBUTE_DIRECTORY)) { + if (!IsDirectoryByAttributes(attr)) { // The path doesn't exist or is not a directory. return false; } @@ -1129,11 +967,16 @@ bool IsDevNull(const char* path) { (path[2] == 'L' || path[2] == 'l') && path[3] == 0)); } +static bool IsDirectoryByAttributes(DWORD attrs) { + return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY); +} + static bool IsDirectoryW(const wstring& path) { DWORD attrs = ::GetFileAttributesW(path.c_str()); - return (attrs != INVALID_FILE_ATTRIBUTES) && - (attrs & FILE_ATTRIBUTE_DIRECTORY) && - JunctionResolver().Resolve(path.c_str(), nullptr); + return IsDirectoryByAttributes(attrs) && + (!(attrs & FILE_ATTRIBUTE_REPARSE_POINT) || + (RealPath(path, &attrs) != nullptr && + IsDirectoryByAttributes(attrs))); } bool IsDirectory(const string& path) { @@ -1228,8 +1071,8 @@ bool ChangeDirectory(const string& path) { ::SetCurrentDirectoryA(spath.c_str()) == TRUE; } -void ForEachDirectoryEntry(const string &path, - DirectoryEntryConsumer *consume) { +void ForEachDirectoryEntry(const string& path, + DirectoryEntryConsumer* consume) { wstring wpath; if (path.empty() || IsDevNull(path.c_str())) { return; -- cgit v1.2.3