From 414a625a790ba87469291d4b3c0a2f6a49b3a467 Mon Sep 17 00:00:00 2001 From: Laszlo Csomor Date: Tue, 14 Aug 2018 16:01:24 +0200 Subject: [PATCH] Windows: expand path names on command line Fixes https://github.com/google/protobuf/issues/3957 --- BUILD | 20 +- .../compiler/command_line_interface.cc | 25 + src/google/protobuf/stubs/io_win32.cc | 91 +++ src/google/protobuf/stubs/io_win32.cc~ | 505 ++++++++++++++ src/google/protobuf/stubs/io_win32.h | 25 + src/google/protobuf/stubs/io_win32.h~ | 140 ++++ .../protobuf/stubs/io_win32_unittest.cc | 191 +++++- .../protobuf/stubs/io_win32_unittest.cc~ | 639 ++++++++++++++++++ 8 files changed, 1615 insertions(+), 21 deletions(-) create mode 100644 src/google/protobuf/stubs/io_win32.cc~ create mode 100644 src/google/protobuf/stubs/io_win32.h~ create mode 100644 src/google/protobuf/stubs/io_win32_unittest.cc~ diff --git a/BUILD b/BUILD index 25c41a426ec9..934c23ed4e1c 100644 --- a/BUILD +++ b/BUILD @@ -362,25 +362,7 @@ cc_library( ], copts = COPTS, includes = ["src/"], - linkopts = LINK_OPTS + select({ - ":msvc": [ - # Linking to setargv.obj makes the default command line argument - # parser expand wildcards, so the main method's argv will contain the - # expanded list instead of the wildcards. - # - # Adding dummy "-DEFAULTLIB:kernel32.lib", because: - # - Microsoft ships this object file next to default libraries - # - but this file is not a library, just a precompiled object - # - "-WHOLEARCHIVE" and "-DEFAULTLIB" only accept library, - # not precompiled object. - # - Bazel would assume linkopt that does not start with "-" or "$" - # as a label to a target, so we add a harmless "-DEFAULTLIB:kernel32.lib" - # before "setargv.obj". - # See https://msdn.microsoft.com/en-us/library/8bch7bkk.aspx - "-DEFAULTLIB:kernel32.lib setargv.obj", - ], - "//conditions:default": [], - }), + linkopts = LINK_OPTS, visibility = ["//visibility:public"], deps = [":protobuf"], ) diff --git a/src/google/protobuf/compiler/command_line_interface.cc b/src/google/protobuf/compiler/command_line_interface.cc index 842a17072dc1..e5fd845978b0 100644 --- a/src/google/protobuf/compiler/command_line_interface.cc +++ b/src/google/protobuf/compiler/command_line_interface.cc @@ -1388,7 +1388,32 @@ CommandLineInterface::InterpretArgument(const string& name, return PARSE_ARGUMENT_FAIL; } +#if defined(_WIN32) + // On Windows, the shell (typically cmd.exe) does not expand wildcards in + // file names (e.g. foo\*.proto), so we do it ourselves. + switch (google::protobuf::internal::win32::expand_wildcards( + value, + [this](const string& path) { + this->input_files_.push_back(path); + })) { + case google::protobuf::internal::win32::ExpandWildcardsResult::kSuccess: + break; + case google::protobuf::internal::win32::ExpandWildcardsResult::kErrorNoMatchingFile: + // Path does not exist, is not a file, or it's longer than MAX_PATH and + // long path handling is disabled. + std::cerr << "Invalid file name pattern or missing input file \"" + << value << "\"" << std::endl; + return PARSE_ARGUMENT_FAIL; + default: + std::cerr << "Cannot convert path \"" << value + << "\" to or from Windows style" << std::endl; + return PARSE_ARGUMENT_FAIL; + } +#else // not _WIN32 + // On other platforms than Windows (e.g. Linux, Mac OS) the shell (typically + // Bash) expands wildcards. input_files_.push_back(value); +#endif // _WIN32 } else if (name == "-I" || name == "--proto_path") { if (!descriptor_set_in_names_.empty()) { diff --git a/src/google/protobuf/stubs/io_win32.cc b/src/google/protobuf/stubs/io_win32.cc index f00a268febc4..4fa95fefb927 100644 --- a/src/google/protobuf/stubs/io_win32.cc +++ b/src/google/protobuf/stubs/io_win32.cc @@ -56,6 +56,11 @@ #include #include #include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif + #include #include @@ -355,6 +360,92 @@ wstring testonly_utf8_to_winpath(const char* path) { return as_windows_path(path, &wpath) ? wpath : wstring(); } +bool expand_wildcards( + const string& path, std::function consume) { + if (path.find_first_of("*?") == string::npos) { + // There are no wildcards in the path, we don't need to expand it. + consume(path); + return ExpandWildcardsResult::kSuccess; + } + +#ifdef SUPPORT_LONGPATHS + + wstring wpath; + if (!as_windows_path(path.c_str(), &wpath)) { + return ExpandWildcardsResult::kErrorInputPathConversion; + } + + static const wstring kDot = L"."; + static const wstring kDotDot = L".."; + WIN32_FIND_DATAW metadata; + HANDLE handle = ::FindFirstFileW(wpath.c_str(), &metadata); + if (handle == INVALID_HANDLE_VALUE) { + // The pattern does not match any files (or directories). + return ExpandWildcardsResult::kErrorNoMatchingFile; + } + + string::size_type pos = path.find_last_of("\\/"); + string dirname; + if (pos != string::npos) { + dirname = path.substr(0, pos + 1); + } + + int matched = ExpandWildcardsResult::kErrorNoMatchingFile; + do { + // Ignore ".", "..", and directories. + if ((metadata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 + && kDot != metadata.cFileName && kDotDot != metadata.cFileName) { + matched = ExpandWildcardsResult::kSuccess; + string filename; + if (!strings::wcs_to_utf8(metadata.cFileName, &filename)) { + return ExpandWildcardsResult::kErrorOutputPathConversion; + } + + if (dirname.empty()) { + consume(filename); + } else { + consume(dirname + filename); + } + } + } while (::FindNextFileW(handle, &metadata)); + FindClose(handle); + return matched; + +#else // not SUPPORT_LONGPATHS + + static const string kDot = "."; + static const string kDotDot = ".."; + WIN32_FIND_DATAA metadata; + HANDLE handle = ::FindFirstFileA(path.c_str(), &metadata); + if (handle == INVALID_HANDLE_VALUE) { + // The pattern does not match any files (or directories). + return ExpandWildcardsResult::kErrorNoMatchingFile; + } + + string::size_type pos = path.find_last_of("\\/"); + string dirname; + if (pos != string::npos) { + dirname = path.substr(0, pos + 1); + } + + int matched = ExpandWildcardsResult::kErrorNoMatchingFile; + do { + // Ignore ".", "..", and directories. + if ((metadata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 + && kDot != metadata.cFileName && kDotDot != metadata.cFileName) { + matched = ExpandWildcardsResult::kSuccess; + if (!dirname.empty()) { + consume(dirname + metadata.cFileName); + } else { + consume(metadata.cFileName); + } + } + } while (::FindNextFileA(handle, &metadata)); + FindClose(handle); + return matched; +#endif // SUPPORT_LONGPATHS +} + namespace strings { bool wcs_to_mbs(const WCHAR* s, string* out, bool outUtf8) { diff --git a/src/google/protobuf/stubs/io_win32.cc~ b/src/google/protobuf/stubs/io_win32.cc~ new file mode 100644 index 000000000000..fa40627185c2 --- /dev/null +++ b/src/google/protobuf/stubs/io_win32.cc~ @@ -0,0 +1,505 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: laszlocsomor@google.com (Laszlo Csomor) +// +// Implementation for long-path-aware open/mkdir/access/etc. on Windows, as well +// as for the supporting utility functions. +// +// These functions convert the input path to an absolute Windows path +// with "\\?\" prefix, then pass that to _wopen/_wmkdir/_waccess/etc. +// (declared in ) respectively. This allows working with files/directories +// whose paths are longer than MAX_PATH (260 chars). +// +// This file is only used on Windows, it's empty on other platforms. + +#if defined(_WIN32) + +// Comment this out to fall back to using the ANSI versions (open, mkdir, ...) +// instead of the Unicode ones (_wopen, _wmkdir, ...). Doing so can be useful to +// debug failing tests if that's caused by the long path support. +// #define SUPPORT_LONGPATHS + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif + +#include + +#include + +#include +#include +#include +#include + +namespace google { +namespace protobuf { +namespace internal { +namespace win32 { +namespace { + +using std::string; +using std::wstring; + +template +struct CharTraits { + static bool is_alpha(char_type ch); +}; + +template <> +struct CharTraits { + static bool is_alpha(char ch) { return isalpha(ch); } +}; + +template <> +struct CharTraits { + static bool is_alpha(wchar_t ch) { return iswalpha(ch); } +}; + +template +bool null_or_empty(const char_type* s) { + return s == nullptr || *s == 0; +} + +// Returns true if the path starts with a drive letter, e.g. "c:". +// Note that this won't check for the "\" after the drive letter, so this also +// returns true for "c:foo" (which is "c:\${PWD}\foo"). +// This check requires that a path not have a longpath prefix ("\\?\"). +template +bool has_drive_letter(const char_type* ch) { + return CharTraits::is_alpha(ch[0]) && ch[1] == ':'; +} + +// Returns true if the path starts with a longpath prefix ("\\?\"). +template +bool has_longpath_prefix(const char_type* path) { + return path[0] == '\\' && path[1] == '\\' && path[2] == '?' && + path[3] == '\\'; +} + +template +bool is_separator(char_type c) { + return c == '/' || c == '\\'; +} + +// Returns true if the path starts with a drive specifier (e.g. "c:\"). +template +bool is_path_absolute(const char_type* path) { + return has_drive_letter(path) && is_separator(path[2]); +} + +template +bool is_drive_relative(const char_type* path) { + return has_drive_letter(path) && (path[2] == 0 || !is_separator(path[2])); +} + +wstring join_paths(const wstring& path1, const wstring& path2) { + if (path1.empty() || is_path_absolute(path2.c_str()) || + has_longpath_prefix(path2.c_str())) { + return path2; + } + if (path2.empty()) { + return path1; + } + + if (is_separator(path1[path1.size() - 1])) { + return is_separator(path2[0]) ? (path1 + path2.substr(1)) + : (path1 + path2); + } else { + return is_separator(path2[0]) ? (path1 + path2) + : (path1 + L'\\' + path2); + } +} + +wstring normalize(wstring path) { + if (has_longpath_prefix(path.c_str())) { + path = path.substr(4); + } + + static const wstring dot(L"."); + static const wstring dotdot(L".."); + const WCHAR* p = path.c_str(); + + std::vector segments; + int segment_start = -1; + // Find the path segments in `path` (separated by "/"). + for (int i = 0;; ++i) { + if (!is_separator(p[i]) && p[i] != L'\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 "..". + wstring segment(p, segment_start, i - segment_start); + segment_start = -1; + if (segment == dotdot) { + if (!segments.empty() && + (!has_drive_letter(segments[0].c_str()) || segments.size() > 1)) { + segments.pop_back(); + } + } else if (segment != dot && !segment.empty()) { + segments.push_back(segment); + } + } + if (p[i] == L'\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 && + has_drive_letter(segments[0].c_str())) { + return segments[0] + L'\\'; + } + + // Join all segments. + bool first = true; + std::wstringstream result; + for (int i = 0; i < segments.size(); ++i) { + if (!first) { + result << L'\\'; + } + first = false; + result << segments[i]; + } + // Preserve trailing separator if the input contained it. + if (!path.empty() && is_separator(p[path.size() - 1])) { + result << L'\\'; + } + return result.str(); +} + +bool as_windows_path(const char* path, wstring* result) { + if (null_or_empty(path)) { + result->clear(); + return true; + } + wstring wpath; + if (!strings::utf8_to_wcs(path, &wpath)) { + return false; + } + if (has_longpath_prefix(wpath.c_str())) { + *result = wpath; + return true; + } + if (is_separator(path[0]) || is_drive_relative(path)) { + return false; + } + + + if (!is_path_absolute(wpath.c_str())) { + int size = ::GetCurrentDirectoryW(0, nullptr); + if (size == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return false; + } + std::unique_ptr wcwd(new WCHAR[size]); + ::GetCurrentDirectoryW(size, wcwd.get()); + wpath = join_paths(wcwd.get(), wpath); + } + wpath = normalize(wpath); + if (!has_longpath_prefix(wpath.c_str())) { + // Add the "\\?\" prefix unconditionally. This way we prevent the Win32 API + // from processing the path and "helpfully" removing trailing dots from the + // path, for example. + // See https://github.com/bazelbuild/bazel/issues/2935 + wpath = wstring(L"\\\\?\\") + wpath; + } + *result = wpath; + return true; +} + +} // namespace + +int open(const char* path, int flags, int mode) { +#ifdef SUPPORT_LONGPATHS + wstring wpath; + if (!as_windows_path(path, &wpath)) { + errno = ENOENT; + return -1; + } + return ::_wopen(wpath.c_str(), flags, mode); +#else + return ::_open(path, flags, mode); +#endif +} + +int mkdir(const char* path, int _mode) { +#ifdef SUPPORT_LONGPATHS + wstring wpath; + if (!as_windows_path(path, &wpath)) { + errno = ENOENT; + return -1; + } + return ::_wmkdir(wpath.c_str()); +#else // not SUPPORT_LONGPATHS + return ::_mkdir(path); +#endif // not SUPPORT_LONGPATHS +} + +int access(const char* path, int mode) { +#ifdef SUPPORT_LONGPATHS + wstring wpath; + if (!as_windows_path(path, &wpath)) { + errno = ENOENT; + return -1; + } + return ::_waccess(wpath.c_str(), mode); +#else + return ::_access(path, mode); +#endif +} + +int chdir(const char* path) { +#ifdef SUPPORT_LONGPATHS + wstring wpath; + if (!as_windows_path(path, &wpath)) { + errno = ENOENT; + return -1; + } + return ::_wchdir(wpath.c_str()); +#else + return ::_chdir(path); +#endif +} + +int stat(const char* path, struct _stat* buffer) { +#ifdef SUPPORT_LONGPATHS + wstring wpath; + if (!as_windows_path(path, &wpath)) { + errno = ENOENT; + return -1; + } + return ::_wstat(wpath.c_str(), buffer); +#else // not SUPPORT_LONGPATHS + return ::_stat(path, buffer); +#endif // not SUPPORT_LONGPATHS +} + +FILE* fopen(const char* path, const char* mode) { +#ifdef SUPPORT_LONGPATHS + if (null_or_empty(path)) { + errno = EINVAL; + return nullptr; + } + wstring wpath; + if (!as_windows_path(path, &wpath)) { + errno = ENOENT; + return nullptr; + } + wstring wmode; + if (!strings::utf8_to_wcs(mode, &wmode)) { + errno = EINVAL; + return nullptr; + } + return ::_wfopen(wpath.c_str(), wmode.c_str()); +#else + return ::fopen(path, mode); +#endif +} + +int close(int fd) { return ::close(fd); } + +int dup(int fd) { return ::_dup(fd); } + +int dup2(int fd1, int fd2) { return ::_dup2(fd1, fd2); } + +int read(int fd, void* buffer, size_t size) { + return ::_read(fd, buffer, size); +} + +int setmode(int fd, int mode) { return ::_setmode(fd, mode); } + +int write(int fd, const void* buffer, size_t size) { + return ::_write(fd, buffer, size); +} + +wstring testonly_utf8_to_winpath(const char* path) { + wstring wpath; + return as_windows_path(path, &wpath) ? wpath : wstring(); +} + +bool expand_wildcards( + const string& path, std::function consume) { + if (path.find_first_of("*?") == string::npos) { + // There are no wildcards in the path, we don't need to expand it. + consume(path); + return ExpandWildcardsResult::kSuccess; + } + +#ifdef SUPPORT_LONGPATHS + + wstring wpath; + if (!as_windows_path(path.c_str(), &wpath)) { + return ExpandWildcardsResult::kErrorInputPathConversion; + } + + static const wstring kDot = L"."; + static const wstring kDotDot = L".."; + WIN32_FIND_DATAW metadata; + HANDLE handle = ::FindFirstFileW(wpath.c_str(), &metadata); + if (handle == INVALID_HANDLE_VALUE) { + // The pattern does not match any files (or directories). + return ExpandWildcardsResult::kErrorNoMatchingFile; + } + + string::size_type pos = path.find_last_of("\\/"); + string dirname; + if (pos != string::npos) { + dirname = path.substr(0, pos + 1); + } + + int matched = ExpandWildcardsResult::kErrorNoMatchingFile; + do { + // Ignore ".", "..", and directories. + if ((metadata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 + && kDot != metadata.cFileName && kDotDot != metadata.cFileName) { + matched = ExpandWildcardsResult::kSuccess; + string filename; + if (!strings::wcs_to_utf8(metadata.cFileName, &filename)) { + return ExpandWildcardsResult::kErrorOutputPathConversion; + } + + if (dirname.empty()) { + consume(filename); + } else { + consume(dirname + filename); + } + } + } while (::FindNextFileW(handle, &metadata)); + FindClose(handle); + return matched; + +#else // not SUPPORT_LONGPATHS + + static const string kDot = "."; + static const string kDotDot = ".."; + WIN32_FIND_DATAA metadata; + HANDLE handle = ::FindFirstFileA(path.c_str(), &metadata); + if (handle == INVALID_HANDLE_VALUE) { + // The pattern does not match any files (or directories). + return ExpandWildcardsResult::kErrorNoMatchingFile; + } + + string::size_type pos = path.find_last_of("\\/"); + string dirname; + if (pos != string::npos) { + dirname = path.substr(0, pos + 1); + } + + int matched = ExpandWildcardsResult::kErrorNoMatchingFile; + do { + // Ignore ".", "..", and directories. + if ((metadata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 + && kDot != metadata.cFileName && kDotDot != metadata.cFileName) { + matched = ExpandWildcardsResult::kSuccess; + if (!dirname.empty()) { + consume(dirname + metadata.cFileName); + } else { + consume(metadata.cFileName); + } + } + } while (::FindNextFileA(handle, &metadata)); + FindClose(handle); + return matched; +#endif // SUPPORT_LONGPATHS +} + +namespace strings { + +bool wcs_to_mbs(const WCHAR* s, string* out, bool outUtf8) { + if (null_or_empty(s)) { + out->clear(); + return true; + } + BOOL usedDefaultChar = FALSE; + SetLastError(0); + int size = WideCharToMultiByte( + outUtf8 ? CP_UTF8 : CP_ACP, 0, s, -1, nullptr, 0, nullptr, + outUtf8 ? nullptr : &usedDefaultChar); + if ((size == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) + || usedDefaultChar) { + return false; + } + std::unique_ptr astr(new CHAR[size]); + WideCharToMultiByte( + outUtf8 ? CP_UTF8 : CP_ACP, 0, s, -1, astr.get(), size, nullptr, nullptr); + out->assign(astr.get()); + return true; +} + +bool mbs_to_wcs(const char* s, wstring* out, bool inUtf8) { + if (null_or_empty(s)) { + out->clear(); + return true; + } + + SetLastError(0); + int size = + MultiByteToWideChar(inUtf8 ? CP_UTF8 : CP_ACP, 0, s, -1, nullptr, 0); + if (size == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return false; + } + std::unique_ptr wstr(new WCHAR[size]); + MultiByteToWideChar( + inUtf8 ? CP_UTF8 : CP_ACP, 0, s, -1, wstr.get(), size + 1); + out->assign(wstr.get()); + return true; +} + +bool utf8_to_wcs(const char* input, wstring* out) { + return mbs_to_wcs(input, out, true); +} + +bool wcs_to_utf8(const wchar_t* input, string* out) { + return wcs_to_mbs(input, out, true); +} + +} // namespace strings +} // namespace win32 +} // namespace internal +} // namespace protobuf +} // namespace google + +#endif // defined(_WIN32) diff --git a/src/google/protobuf/stubs/io_win32.h b/src/google/protobuf/stubs/io_win32.h index 9e17d25304d2..f96746fb7ce5 100644 --- a/src/google/protobuf/stubs/io_win32.h +++ b/src/google/protobuf/stubs/io_win32.h @@ -47,6 +47,7 @@ #if defined(_WIN32) +#include #include #include @@ -71,6 +72,30 @@ LIBPROTOBUF_EXPORT int stat(const char* path, struct _stat* buffer); LIBPROTOBUF_EXPORT int write(int fd, const void* buffer, size_t size); LIBPROTOBUF_EXPORT std::wstring testonly_utf8_to_winpath(const char* path); +struct ExpandWildcardsResult { + enum { + kSuccess = 0, + kErrorNoMatchingFile = 1, + kErrorInputPathConversion = 2, + kErrorOutputPathConversion = 3, + }; +}; + +// Expand wildcards in a path pattern, feed the result to a consumer function. +// +// `path` must be a valid, Windows-style path. It may be absolute, or relative +// to the current working directory, and it may contain wildcards ("*" and "?") +// in the last path segment. This function passes all matching file names to +// `consume`. The resulting paths may not be absolute nor normalized. +// +// The function returns true if the path did not contain any wildcards (in +// which case the path may or may not exist), or the path did contain wildcards +// and it matched at least one file. +// The function returns false if the path contained wildcards but it did not +// match any files. +LIBPROTOBUF_EXPORT bool expand_wildcards( + const std::string& path, std::function consume); + namespace strings { // Convert from UTF-16 to Active-Code-Page-encoded or to UTF-8-encoded text. diff --git a/src/google/protobuf/stubs/io_win32.h~ b/src/google/protobuf/stubs/io_win32.h~ new file mode 100644 index 000000000000..3e2c6099d988 --- /dev/null +++ b/src/google/protobuf/stubs/io_win32.h~ @@ -0,0 +1,140 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: laszlocsomor@google.com (Laszlo Csomor) +// +// This file contains the declarations for Windows implementations of +// commonly used POSIX functions such as open(2) and access(2), as well +// as macro definitions for flags of these functions. +// +// By including this file you'll redefine open/access/etc. to +// ::google::protobuf::internal::win32::{open/access/etc.}. +// Make sure you don't include a header that attempts to redeclare or +// redefine these functions, that'll lead to confusing compilation +// errors. It's best to #include this file as the last one to ensure that. +// +// This file is only used on Windows, it's empty on other platforms. + +#ifndef GOOGLE_PROTOBUF_STUBS_IO_WIN32_H__ +#define GOOGLE_PROTOBUF_STUBS_IO_WIN32_H__ + +#if defined(_WIN32) + +#include +#include +#include + +// Compilers on Windows other than MSVC (e.g. Cygwin, MinGW32) define the +// following functions already, except for mkdir. +namespace google { +namespace protobuf { +namespace internal { +namespace win32 { + +LIBPROTOBUF_EXPORT FILE* fopen(const char* path, const char* mode); +LIBPROTOBUF_EXPORT int access(const char* path, int mode); +LIBPROTOBUF_EXPORT int chdir(const char* path); +LIBPROTOBUF_EXPORT int close(int fd); +LIBPROTOBUF_EXPORT int dup(int fd); +LIBPROTOBUF_EXPORT int dup2(int fd1, int fd2); +LIBPROTOBUF_EXPORT int mkdir(const char* path, int _mode); +LIBPROTOBUF_EXPORT int open(const char* path, int flags, int mode = 0); +LIBPROTOBUF_EXPORT int read(int fd, void* buffer, size_t size); +LIBPROTOBUF_EXPORT int setmode(int fd, int mode); +LIBPROTOBUF_EXPORT int stat(const char* path, struct _stat* buffer); +LIBPROTOBUF_EXPORT int write(int fd, const void* buffer, size_t size); +LIBPROTOBUF_EXPORT std::wstring testonly_utf8_to_winpath(const char* path); + +struct ExpandWildcardsResult { + enum { + kSuccess = 0, + kErrorNoMatchingFile = 1, + kErrorInputPathConversion = 2, + kErrorResultPathConversion = 3, + }; +}; + +// Expand wildcards in a path pattern, feed the result to a consumer function. +// +// `path` must be a valid, Windows-style path. It may be absolute, or relative +// to the current working directory, and it may contain wildcards ("*" and "?") +// in the last path segment. This function passes all matching file names to +// `consume`. The resulting paths may not be absolute nor normalized. +// +// The function returns true if the path did not contain any wildcards (in +// which case the path may or may not exist), or the path did contain wildcards +// and it matched at least one file. +// The function returns false if the path contained wildcards but it did not +// match any files. +LIBPROTOBUF_EXPORT bool expand_wildcards( + const std::string& path, std::function consume); + +namespace strings { + +// Convert from UTF-16 to Active-Code-Page-encoded or to UTF-8-encoded text. +LIBPROTOBUF_EXPORT bool wcs_to_mbs( + const wchar_t* s, std::string* out, bool outUtf8); + +// Convert from Active-Code-Page-encoded or UTF-8-encoded text to UTF-16. +LIBPROTOBUF_EXPORT bool mbs_to_wcs( + const char* s, std::wstring* out, bool inUtf8); + +// Convert from UTF-8-encoded text to UTF-16. +LIBPROTOBUF_EXPORT bool utf8_to_wcs(const char* input, std::wstring* out); + +// Convert from UTF-16-encoded text to UTF-8. +LIBPROTOBUF_EXPORT bool wcs_to_utf8(const wchar_t* input, std::string* out); + +} // namespace strings + +} // namespace win32 +} // namespace internal +} // namespace protobuf +} // namespace google + +#ifndef W_OK +#define W_OK 02 // not defined by MSVC for whatever reason +#endif + +#ifndef F_OK +#define F_OK 00 // not defined by MSVC for whatever reason +#endif + +#ifndef STDIN_FILENO +#define STDIN_FILENO 0 +#endif + +#ifndef STDOUT_FILENO +#define STDOUT_FILENO 1 +#endif + +#endif // defined(_WIN32) + +#endif // GOOGLE_PROTOBUF_STUBS_IO_WIN32_H__ diff --git a/src/google/protobuf/stubs/io_win32_unittest.cc b/src/google/protobuf/stubs/io_win32_unittest.cc index 6c70d461b756..39e36d91433a 100644 --- a/src/google/protobuf/stubs/io_win32_unittest.cc +++ b/src/google/protobuf/stubs/io_win32_unittest.cc @@ -53,6 +53,7 @@ #include #include #include +#include namespace google { namespace protobuf { @@ -82,6 +83,7 @@ const wchar_t kUtf16Text[] = { }; using std::string; +using std::vector; using std::wstring; class IoWin32Test : public ::testing::Test { @@ -143,12 +145,24 @@ bool GetCwdAsUtf8(string* result) { } } +bool CreateEmptyFile(const wstring& path) { + HANDLE h = CreateFileW(path.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + if (h == INVALID_HANDLE_VALUE) { + return false; + } + CloseHandle(h); + return true; +} + } // namespace void IoWin32Test::SetUp() { test_tmpdir.clear(); wtest_tmpdir.clear(); - EXPECT_GT(::GetCurrentDirectoryW(MAX_PATH, working_directory), 0); + DWORD size = ::GetCurrentDirectoryW(MAX_PATH, working_directory); + EXPECT_GT(size, 0); + EXPECT_LT(size, MAX_PATH); string tmp; bool ok = false; @@ -351,7 +365,7 @@ TEST_F(IoWin32Test, MkdirTestNonAscii) { ASSERT_INITIALIZED; // Create a non-ASCII path. - // Ensure that we can create the directory using SetCurrentDirectoryW. + // Ensure that we can create the directory using CreateDirectoryW. EXPECT_TRUE(CreateDirectoryW((wtest_tmpdir + L"\\1").c_str(), nullptr)); EXPECT_TRUE(CreateDirectoryW((wtest_tmpdir + L"\\1\\" + kUtf16Text).c_str(), nullptr)); // Ensure that we can create a very similarly named directory using mkdir. @@ -399,6 +413,179 @@ TEST_F(IoWin32Test, ChdirTestNonAscii) { ASSERT_EQ(wNonAscii, cwd); } +TEST_F(IoWin32Test, ExpandWildcardsInRelativePathTest) { + wstring wNonAscii(wtest_tmpdir + L"\\" + kUtf16Text); + EXPECT_TRUE(CreateDirectoryW(wNonAscii.c_str(), nullptr)); + // Create mock files we will test pattern matching on. + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_a.proto")); + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_b.proto")); + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\bar.proto")); + // `cd` into `wtest_tmpdir`. + EXPECT_TRUE(SetCurrentDirectoryW(wtest_tmpdir.c_str())); + + int found_a = 0; + int found_b = 0; + vector found_bad; + // Assert matching a relative path pattern. Results should also be relative. + int result = + expand_wildcards( + string(kUtf8Text) + "\\foo*.proto", + [&found_a, &found_b, &found_bad](const string& p) { + if (p == string(kUtf8Text) + "\\foo_a.proto") { + found_a++; + } else if (p == string(kUtf8Text) + "\\foo_b.proto") { + found_b++; + } else { + found_bad.push_back(p); + } + }); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + EXPECT_EQ(found_a, 1); + EXPECT_EQ(found_b, 1); + if (!found_bad.empty()) { + FAIL() << found_bad[0]; + } + + // Assert matching the exact filename. + found_a = 0; + found_bad.clear(); + result = + expand_wildcards( + string(kUtf8Text) + "\\foo_a.proto", + [&found_a, &found_bad](const string& p) { + if (p == string(kUtf8Text) + "\\foo_a.proto") { + found_a++; + } else { + found_bad.push_back(p); + } + }); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + EXPECT_EQ(found_a, 1); + if (!found_bad.empty()) { + FAIL() << found_bad[0]; + } +} + +TEST_F(IoWin32Test, ExpandWildcardsInAbsolutePathTest) { + wstring wNonAscii(wtest_tmpdir + L"\\" + kUtf16Text); + EXPECT_TRUE(CreateDirectoryW(wNonAscii.c_str(), nullptr)); + // Create mock files we will test pattern matching on. + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_a.proto")); + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_b.proto")); + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\bar.proto")); + + int found_a = 0; + int found_b = 0; + vector found_bad; + // Assert matching an absolute path. The results should also use absolute + // path. + int result = + expand_wildcards( + string(test_tmpdir) + "\\" + kUtf8Text + "\\foo*.proto", + [this, &found_a, &found_b, &found_bad](const string& p) { + if (p == string(this->test_tmpdir) + + "\\" + + kUtf8Text + + "\\foo_a.proto") { + found_a++; + } else if (p == string(this->test_tmpdir) + + "\\" + + kUtf8Text + + "\\foo_b.proto") { + found_b++; + } else { + found_bad.push_back(p); + } + }); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + EXPECT_EQ(found_a, 1); + EXPECT_EQ(found_b, 1); + if (!found_bad.empty()) { + FAIL() << found_bad[0]; + } + + // Assert matching the exact filename. + found_a = 0; + found_bad.clear(); + result = + expand_wildcards( + string(test_tmpdir) + "\\" + kUtf8Text + "\\foo_a.proto", + [this, &found_a, &found_bad](const string& p) { + if (p == string(this->test_tmpdir) + + "\\" + + kUtf8Text + + "\\foo_a.proto") { + found_a++; + } else { + found_bad.push_back(p); + } + }); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + EXPECT_EQ(found_a, 1); + if (!found_bad.empty()) { + FAIL() << found_bad[0]; + } +} + +TEST_F(IoWin32Test, ExpandWildcardsIgnoresDirectoriesTest) { + wstring wNonAscii(wtest_tmpdir + L"\\" + kUtf16Text); + EXPECT_TRUE(CreateDirectoryW(wNonAscii.c_str(), nullptr)); + // Create mock files we will test pattern matching on. + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_a.proto")); + EXPECT_TRUE(CreateDirectoryW((wNonAscii + L"\\foo_b.proto").c_str(), nullptr)); + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_c.proto")); + // `cd` into `wtest_tmpdir`. + EXPECT_TRUE(SetCurrentDirectoryW(wtest_tmpdir.c_str())); + + int found_a = 0; + int found_c = 0; + vector found_bad; + // Assert that the pattern matches exactly the expected files, and using the + // absolute path as did the input pattern. + int result = + expand_wildcards( + string(kUtf8Text) + "\\foo*.proto", + [&found_a, &found_c, &found_bad](const string& p) { + if (p == string(kUtf8Text) + "\\foo_a.proto") { + found_a++; + } else if (p == string(kUtf8Text) + "\\foo_c.proto") { + found_c++; + } else { + found_bad.push_back(p); + } + }); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + EXPECT_EQ(found_a, 1); + EXPECT_EQ(found_c, 1); + if (!found_bad.empty()) { + FAIL() << found_bad[0]; + } +} + +TEST_F(IoWin32Test, ExpandWildcardsFailsIfNoFileMatchesTest) { + wstring wNonAscii(wtest_tmpdir + L"\\" + kUtf16Text); + EXPECT_TRUE(CreateDirectoryW(wNonAscii.c_str(), nullptr)); + // Create mock files we will test pattern matching on. + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_a.proto")); + // `cd` into `wtest_tmpdir`. + EXPECT_TRUE(SetCurrentDirectoryW(wtest_tmpdir.c_str())); + + // Control test: should match foo*.proto + int result = expand_wildcards( + string(kUtf8Text) + "\\foo*.proto", [](const string&) {}); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + + // Control test: should match foo_a.proto + result = expand_wildcards( + string(kUtf8Text) + "\\foo_a.proto", [](const string&) {}); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + + // Actual test: should not match anything. + result = expand_wildcards( + string(kUtf8Text) + "\\bar*.proto", [](const string&) {}); + ASSERT_EQ(result, ExpandWildcardsResult::kErrorNoMatchingFile); +} + TEST_F(IoWin32Test, AsWindowsPathTest) { DWORD size = GetCurrentDirectoryW(0, nullptr); std::unique_ptr cwd_str(new wchar_t[size]); diff --git a/src/google/protobuf/stubs/io_win32_unittest.cc~ b/src/google/protobuf/stubs/io_win32_unittest.cc~ new file mode 100644 index 000000000000..ae3436780084 --- /dev/null +++ b/src/google/protobuf/stubs/io_win32_unittest.cc~ @@ -0,0 +1,639 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: laszlocsomor@google.com (Laszlo Csomor) +// +// Unit tests for long-path-aware open/mkdir/access/etc. on Windows, as well as +// for the supporting utility functions. +// +// This file is only used on Windows, it's empty on other platforms. + +#if defined(_WIN32) + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace google { +namespace protobuf { +namespace internal { +namespace win32 { +namespace { + +const char kUtf8Text[] = { + 'h', 'i', ' ', + // utf-8: 11010000 10011111, utf-16: 100 0001 1111 = 0x041F + static_cast(0xd0), static_cast(0x9f), + // utf-8: 11010001 10000000, utf-16: 100 0100 0000 = 0x0440 + static_cast(0xd1), static_cast(0x80), + // utf-8: 11010000 10111000, utf-16: 100 0011 1000 = 0x0438 + static_cast(0xd0), static_cast(0xb8), + // utf-8: 11010000 10110010, utf-16: 100 0011 0010 = 0x0432 + static_cast(0xd0), static_cast(0xb2), + // utf-8: 11010000 10110101, utf-16: 100 0011 0101 = 0x0435 + static_cast(0xd0), static_cast(0xb5), + // utf-8: 11010001 10000010, utf-16: 100 0100 0010 = 0x0442 + static_cast(0xd1), static_cast(0x82), 0 +}; + +const wchar_t kUtf16Text[] = { + L'h', L'i', L' ', + L'\x41f', L'\x440', L'\x438', L'\x432', L'\x435', L'\x442', 0 +}; + +using std::string; +using std::vector; +using std::wstring; + +class IoWin32Test : public ::testing::Test { + public: + void SetUp(); + void TearDown(); + + protected: + bool CreateAllUnder(wstring path); + bool DeleteAllUnder(wstring path); + + WCHAR working_directory[MAX_PATH]; + string test_tmpdir; + wstring wtest_tmpdir; +}; + +#define ASSERT_INITIALIZED \ + { \ + EXPECT_FALSE(test_tmpdir.empty()); \ + EXPECT_FALSE(wtest_tmpdir.empty()); \ + } + +namespace { +void StripTrailingSlashes(string* str) { + int i = str->size() - 1; + for (; i >= 0 && ((*str)[i] == '/' || (*str)[i] == '\\'); --i) {} + str->resize(i+1); +} + +bool GetEnvVarAsUtf8(const WCHAR* name, string* result) { + DWORD size = ::GetEnvironmentVariableW(name, nullptr, 0); + if (size > 0 && GetLastError() != ERROR_ENVVAR_NOT_FOUND) { + std::unique_ptr wcs(new WCHAR[size]); + ::GetEnvironmentVariableW(name, wcs.get(), size); + // GetEnvironmentVariableA retrieves an Active-Code-Page-encoded text which + // we'd first need to convert to UTF-16 then to UTF-8, because there seems + // to be no API function to do that conversion directly. + // GetEnvironmentVariableW retrieves an UTF-16-encoded text, which we need + // to convert to UTF-8. + return strings::wcs_to_utf8(wcs.get(), result); + } else { + return false; + } +} + +bool GetCwdAsUtf8(string* result) { + DWORD size = ::GetCurrentDirectoryW(0, nullptr); + if (size > 0) { + std::unique_ptr wcs(new WCHAR[size]); + ::GetCurrentDirectoryW(size, wcs.get()); + // GetCurrentDirectoryA retrieves an Active-Code-Page-encoded text which + // we'd first need to convert to UTF-16 then to UTF-8, because there seems + // to be no API function to do that conversion directly. + // GetCurrentDirectoryW retrieves an UTF-16-encoded text, which we need + // to convert to UTF-8. + return strings::wcs_to_utf8(wcs.get(), result); + } else { + return false; + } +} + +bool CreateEmptyFile(const wstring& path) { + HANDLE h = CreateFileW(path.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + if (h == INVALID_HANDLE_VALUE) { + return false; + } + CloseHandle(h); + return true; +} + +} // namespace + +void IoWin32Test::SetUp() { + test_tmpdir.clear(); + wtest_tmpdir.clear(); + DWORD size = ::GetCurrentDirectoryW(MAX_PATH, working_directory); + EXPECT_GT(size, 0); + EXPECT_LT(size, MAX_PATH); + + string tmp; + bool ok = false; + if (!ok) { + // Bazel sets this environment variable when it runs tests. + ok = GetEnvVarAsUtf8(L"TEST_TMPDIR", &tmp); + } + if (!ok) { + // Bazel 0.8.0 sets this environment for every build and test action. + ok = GetEnvVarAsUtf8(L"TEMP", &tmp); + } + if (!ok) { + // Bazel 0.8.0 sets this environment for every build and test action. + ok = GetEnvVarAsUtf8(L"TMP", &tmp); + } + if (!ok) { + // Fall back to using the current directory. + ok = GetCwdAsUtf8(&tmp); + } + if (!ok || tmp.empty()) { + FAIL() << "Cannot find a temp directory."; + } + + StripTrailingSlashes(&tmp); + std::stringstream result; + // Deleting files and directories is asynchronous on Windows, and if TearDown + // just deleted the previous temp directory, sometimes we cannot recreate the + // same directory. + // Use a counter so every test method gets its own temp directory. + static unsigned int counter = 0; + result << tmp << "\\w32tst" << counter++ << ".tmp"; + test_tmpdir = result.str(); + wtest_tmpdir = testonly_utf8_to_winpath(test_tmpdir.c_str()); + ASSERT_FALSE(wtest_tmpdir.empty()); + ASSERT_TRUE(DeleteAllUnder(wtest_tmpdir)); + ASSERT_TRUE(CreateAllUnder(wtest_tmpdir)); +} + +void IoWin32Test::TearDown() { + if (!wtest_tmpdir.empty()) { + DeleteAllUnder(wtest_tmpdir); + } + ::SetCurrentDirectoryW(working_directory); +} + +bool IoWin32Test::CreateAllUnder(wstring path) { + // Prepend UNC prefix if the path doesn't have it already. Don't bother + // checking if the path is shorter than MAX_PATH, let's just do it + // unconditionally. + if (path.find(L"\\\\?\\") != 0) { + path = wstring(L"\\\\?\\") + path; + } + if (::CreateDirectoryW(path.c_str(), nullptr) || + GetLastError() == ERROR_ALREADY_EXISTS || + GetLastError() == ERROR_ACCESS_DENIED) { + return true; + } + if (GetLastError() == ERROR_PATH_NOT_FOUND) { + size_t pos = path.find_last_of(L'\\'); + if (pos != wstring::npos) { + wstring parent(path, 0, pos); + if (CreateAllUnder(parent) && CreateDirectoryW(path.c_str(), nullptr)) { + return true; + } + } + } + return false; +} + +bool IoWin32Test::DeleteAllUnder(wstring path) { + static const wstring kDot(L"."); + static const wstring kDotDot(L".."); + + // Prepend UNC prefix if the path doesn't have it already. Don't bother + // checking if the path is shorter than MAX_PATH, let's just do it + // unconditionally. + if (path.find(L"\\\\?\\") != 0) { + path = wstring(L"\\\\?\\") + path; + } + // Append "\" if necessary. + if (path[path.size() - 1] != L'\\') { + path.push_back(L'\\'); + } + + WIN32_FIND_DATAW metadata; + HANDLE handle = ::FindFirstFileW((path + L"*").c_str(), &metadata); + if (handle == INVALID_HANDLE_VALUE) { + return true; // directory doesn't exist + } + + bool result = true; + do { + wstring childname = metadata.cFileName; + if (kDot != childname && kDotDot != childname) { + wstring childpath = path + childname; + if ((metadata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + // If this is not a junction, delete its contents recursively. + // Finally delete this directory/junction too. + if (((metadata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0 && + !DeleteAllUnder(childpath)) || + !::RemoveDirectoryW(childpath.c_str())) { + result = false; + break; + } + } else { + if (!::DeleteFileW(childpath.c_str())) { + result = false; + break; + } + } + } + } while (::FindNextFileW(handle, &metadata)); + ::FindClose(handle); + return result; +} + +TEST_F(IoWin32Test, AccessTest) { + ASSERT_INITIALIZED; + + string path = test_tmpdir; + while (path.size() < MAX_PATH - 30) { + path += "\\accesstest"; + EXPECT_EQ(mkdir(path.c_str(), 0644), 0); + } + string file = path + "\\file.txt"; + int fd = open(file.c_str(), O_CREAT | O_WRONLY, 0644); + if (fd > 0) { + EXPECT_EQ(close(fd), 0); + } else { + EXPECT_TRUE(false); + } + + EXPECT_EQ(access(test_tmpdir.c_str(), F_OK), 0); + EXPECT_EQ(access(path.c_str(), F_OK), 0); + EXPECT_EQ(access(path.c_str(), W_OK), 0); + EXPECT_EQ(access(file.c_str(), F_OK | W_OK), 0); + EXPECT_NE(access((file + ".blah").c_str(), F_OK), 0); + EXPECT_NE(access((file + ".blah").c_str(), W_OK), 0); + + EXPECT_EQ(access(".", F_OK), 0); + EXPECT_EQ(access(".", W_OK), 0); + EXPECT_EQ(access((test_tmpdir + "/accesstest").c_str(), F_OK | W_OK), 0); + ASSERT_EQ(access((test_tmpdir + "/./normalize_me/.././accesstest").c_str(), + F_OK | W_OK), + 0); + EXPECT_NE(access("io_win32_unittest.AccessTest.nonexistent", F_OK), 0); + EXPECT_NE(access("io_win32_unittest.AccessTest.nonexistent", W_OK), 0); + + ASSERT_EQ(access("c:bad", F_OK), -1); + ASSERT_EQ(errno, ENOENT); + ASSERT_EQ(access("/tmp/bad", F_OK), -1); + ASSERT_EQ(errno, ENOENT); + ASSERT_EQ(access("\\bad", F_OK), -1); + ASSERT_EQ(errno, ENOENT); +} + +TEST_F(IoWin32Test, OpenTest) { + ASSERT_INITIALIZED; + + string path = test_tmpdir; + while (path.size() < MAX_PATH) { + path += "\\opentest"; + EXPECT_EQ(mkdir(path.c_str(), 0644), 0); + } + string file = path + "\\file.txt"; + int fd = open(file.c_str(), O_CREAT | O_WRONLY, 0644); + if (fd > 0) { + EXPECT_EQ(write(fd, "hello", 5), 5); + EXPECT_EQ(close(fd), 0); + } else { + EXPECT_TRUE(false); + } + + ASSERT_EQ(open("c:bad.txt", O_CREAT | O_WRONLY, 0644), -1); + ASSERT_EQ(errno, ENOENT); + ASSERT_EQ(open("/tmp/bad.txt", O_CREAT | O_WRONLY, 0644), -1); + ASSERT_EQ(errno, ENOENT); + ASSERT_EQ(open("\\bad.txt", O_CREAT | O_WRONLY, 0644), -1); + ASSERT_EQ(errno, ENOENT); +} + +TEST_F(IoWin32Test, MkdirTest) { + ASSERT_INITIALIZED; + + string path = test_tmpdir; + do { + path += "\\mkdirtest"; + ASSERT_EQ(mkdir(path.c_str(), 0644), 0); + } while (path.size() <= MAX_PATH); + + ASSERT_EQ(mkdir("c:bad", 0644), -1); + ASSERT_EQ(errno, ENOENT); + ASSERT_EQ(mkdir("/tmp/bad", 0644), -1); + ASSERT_EQ(errno, ENOENT); + ASSERT_EQ(mkdir("\\bad", 0644), -1); + ASSERT_EQ(errno, ENOENT); +} + +TEST_F(IoWin32Test, MkdirTestNonAscii) { + ASSERT_INITIALIZED; + + // Create a non-ASCII path. + // Ensure that we can create the directory using CreateDirectoryW. + EXPECT_TRUE(CreateDirectoryW((wtest_tmpdir + L"\\1").c_str(), nullptr)); + EXPECT_TRUE(CreateDirectoryW((wtest_tmpdir + L"\\1\\" + kUtf16Text).c_str(), nullptr)); + // Ensure that we can create a very similarly named directory using mkdir. + // We don't attemp to delete and recreate the same directory, because on + // Windows, deleting files and directories seems to be asynchronous. + EXPECT_EQ(mkdir((test_tmpdir + "\\2").c_str(), 0644), 0); + EXPECT_EQ(mkdir((test_tmpdir + "\\2\\" + kUtf8Text).c_str(), 0644), 0); +} + +TEST_F(IoWin32Test, ChdirTest) { + string path("C:\\"); + EXPECT_EQ(access(path.c_str(), F_OK), 0); + ASSERT_EQ(chdir(path.c_str()), 0); + + // Do not try to chdir into the test_tmpdir, it may already contain directory + // names with trailing dots. + // Instead test here with an obviously dot-trailed path. If the win32_chdir + // function would not convert the path to absolute and prefix with "\\?\" then + // the Win32 API would ignore the trailing dot, but because of the prefixing + // there'll be no path processing done, so we'll actually attempt to chdir + // into "C:\some\path\foo." + path = test_tmpdir + "/foo."; + EXPECT_EQ(mkdir(path.c_str(), 644), 0); + EXPECT_EQ(access(path.c_str(), F_OK), 0); + ASSERT_NE(chdir(path.c_str()), 0); +} + +TEST_F(IoWin32Test, ChdirTestNonAscii) { + ASSERT_INITIALIZED; + + // Create a directory with a non-ASCII path and ensure we can cd into it. + wstring wNonAscii(wtest_tmpdir + L"\\" + kUtf16Text); + string nonAscii; + EXPECT_TRUE(strings::wcs_to_utf8(wNonAscii.c_str(), &nonAscii)); + EXPECT_TRUE(CreateDirectoryW(wNonAscii.c_str(), nullptr)); + WCHAR cwd[MAX_PATH]; + EXPECT_TRUE(GetCurrentDirectoryW(MAX_PATH, cwd)); + // Ensure that we can cd into the path using SetCurrentDirectoryW. + EXPECT_TRUE(SetCurrentDirectoryW(wNonAscii.c_str())); + EXPECT_TRUE(SetCurrentDirectoryW(cwd)); + // Ensure that we can cd into the path using chdir. + ASSERT_EQ(chdir(nonAscii.c_str()), 0); + // Ensure that the GetCurrentDirectoryW returns the desired path. + EXPECT_TRUE(GetCurrentDirectoryW(MAX_PATH, cwd)); + ASSERT_EQ(wNonAscii, cwd); +} + +TEST_F(IoWin32Test, ExpandWildcardsInRelativePathTest) { + wstring wNonAscii(wtest_tmpdir + L"\\" + kUtf16Text); + EXPECT_TRUE(CreateDirectoryW(wNonAscii.c_str(), nullptr)); + // Create mock files we will test pattern matching on. + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_a.proto")); + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_b.proto")); + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\bar.proto")); + // `cd` into `wtest_tmpdir`. + EXPECT_TRUE(SetCurrentDirectoryW(wtest_tmpdir.c_str())); + + int found_a = 0; + int found_b = 0; + vector found_bad; + // Assert matching a relative path pattern. Results should also be relative. + int result = + expand_wildcards( + string(kUtf8Text) + "\\foo*.proto", + [&found_a, &found_b, &found_bad](const string& p) { + if (p == string(kUtf8Text) + "\\foo_a.proto") { + found_a++; + } else if (p == string(kUtf8Text) + "\\foo_b.proto") { + found_b++; + } else { + found_bad.push_back(p); + } + }); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + EXPECT_EQ(found_a, 1); + EXPECT_EQ(found_b, 1); + if (!found_bad.empty()) { + FAIL() << found_bad[0]; + } + + // Assert matching the exact filename. + found_a = 0; + found_bad.clear(); + result = + expand_wildcards( + string(kUtf8Text) + "\\foo_a.proto", + [&found_a, &found_bad](const string& p) { + if (p == string(kUtf8Text) + "\\foo_a.proto") { + found_a++; + } else { + found_bad.push_back(p); + } + }); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + EXPECT_EQ(found_a, 1); + if (!found_bad.empty()) { + FAIL() << found_bad[0]; + } +} + +TEST_F(IoWin32Test, ExpandWildcardsInAbsolutePathTest) { + wstring wNonAscii(wtest_tmpdir + L"\\" + kUtf16Text); + EXPECT_TRUE(CreateDirectoryW(wNonAscii.c_str(), nullptr)); + // Create mock files we will test pattern matching on. + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_a.proto")); + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_b.proto")); + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\bar.proto")); + + int found_a = 0; + int found_b = 0; + vector found_bad; + // Assert matching an absolute path. The results should also use absolute + // path. + int result = + expand_wildcards( + string(test_tmpdir) + "\\" + kUtf8Text + "\\foo*.proto", + [this, &found_a, &found_b, &found_bad](const string& p) { + if (p == string(this->test_tmpdir) + + "\\" + + kUtf8Text + + "\\foo_a.proto") { + found_a++; + } else if (p == string(this->test_tmpdir) + + "\\" + + kUtf8Text + + "\\foo_b.proto") { + found_b++; + } else { + found_bad.push_back(p); + } + }); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + EXPECT_EQ(found_a, 1); + EXPECT_EQ(found_b, 1); + if (!found_bad.empty()) { + FAIL() << found_bad[0]; + } + + // Assert matching the exact filename. + found_a = 0; + found_bad.clear(); + result = + expand_wildcards( + string(test_tmpdir) + "\\" + kUtf8Text + "\\foo_a.proto", + [this, &found_a, &found_bad](const string& p) { + if (p == string(this->test_tmpdir) + + "\\" + + kUtf8Text + + "\\foo_a.proto") { + found_a++; + } else { + found_bad.push_back(p); + } + }); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + EXPECT_EQ(found_a, 1); + if (!found_bad.empty()) { + FAIL() << found_bad[0]; + } +} + +TEST_F(IoWin32Test, ExpandWildcardsIgnoresDirectoriesTest) { + wstring wNonAscii(wtest_tmpdir + L"\\" + kUtf16Text); + EXPECT_TRUE(CreateDirectoryW(wNonAscii.c_str(), nullptr)); + // Create mock files we will test pattern matching on. + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_a.proto")); + EXPECT_TRUE(CreateDirectoryW((wNonAscii + L"\\foo_b.proto").c_str(), nullptr)); + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_c.proto")); + // `cd` into `wtest_tmpdir`. + EXPECT_TRUE(SetCurrentDirectoryW(wtest_tmpdir.c_str())); + + int found_a = 0; + int found_c = 0; + vector found_bad; + // Assert that the pattern matches exactly the expected files, and using the + // absolute path as did the input pattern. + int result = + expand_wildcards( + string(kUtf8Text) + "\\foo*.proto", + [&found_a, &found_c, &found_bad](const string& p) { + if (p == string(kUtf8Text) + "\\foo_a.proto") { + found_a++; + } else if (p == string(kUtf8Text) + "\\foo_c.proto") { + found_c++; + } else { + found_bad.push_back(p); + } + }); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + EXPECT_EQ(found_a, 1); + EXPECT_EQ(found_c, 1); + if (!found_bad.empty()) { + FAIL() << found_bad[0]; + } +} + +TEST_F(IoWin32Test, ExpandWildcardsFailsIfNoFileMatchesTest) { + wstring wNonAscii(wtest_tmpdir + L"\\" + kUtf16Text); + EXPECT_TRUE(CreateDirectoryW(wNonAscii.c_str(), nullptr)); + // Create mock files we will test pattern matching on. + EXPECT_TRUE(CreateEmptyFile(wNonAscii + L"\\foo_a.proto")); + // `cd` into `wtest_tmpdir`. + EXPECT_TRUE(SetCurrentDirectoryW(wtest_tmpdir.c_str())); + + // Control test: should match foo*.proto + int result = expand_wildcards( + string(kUtf8Text) + "\\foo*.proto", [](const string&) {}); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + + // Control test: should match foo_a.proto + result = expand_wildcards( + string(kUtf8Text) + "\\foo_a.proto", [](const string&) {}); + EXPECT_EQ(result, ExpandWildcardsResult::kSuccess); + + // Actual test: should not match anything. + int result = expand_wildcards( + string(kUtf8Text) + "\\bar*.proto", [](const string&) {}); + ASSERT_EQ(result, ExpandWildcardsResult::kErrorNoMatchingFile); +} + +TEST_F(IoWin32Test, AsWindowsPathTest) { + DWORD size = GetCurrentDirectoryW(0, nullptr); + std::unique_ptr cwd_str(new wchar_t[size]); + EXPECT_GT(GetCurrentDirectoryW(size, cwd_str.get()), 0); + wstring cwd = wstring(L"\\\\?\\") + cwd_str.get(); + + ASSERT_EQ(testonly_utf8_to_winpath("relative_mkdirtest"), + cwd + L"\\relative_mkdirtest"); + ASSERT_EQ(testonly_utf8_to_winpath("preserve//\\trailing///"), + cwd + L"\\preserve\\trailing\\"); + ASSERT_EQ(testonly_utf8_to_winpath("./normalize_me\\/../blah"), + cwd + L"\\blah"); + std::ostringstream relpath; + for (wchar_t* p = cwd_str.get(); *p; ++p) { + if (*p == '/' || *p == '\\') { + relpath << "../"; + } + } + relpath << ".\\/../\\./beyond-toplevel"; + ASSERT_EQ(testonly_utf8_to_winpath(relpath.str().c_str()), + wstring(L"\\\\?\\") + cwd_str.get()[0] + L":\\beyond-toplevel"); + + // Absolute unix paths lack drive letters, driveless absolute windows paths + // do too. Neither can be converted to a drive-specifying absolute Windows + // path. + ASSERT_EQ(testonly_utf8_to_winpath("/absolute/unix/path"), L""); + // Though valid on Windows, we also don't support UNC paths (\\UNC\\blah). + ASSERT_EQ(testonly_utf8_to_winpath("\\driveless\\absolute"), L""); + // Though valid in cmd.exe, drive-relative paths are not supported. + ASSERT_EQ(testonly_utf8_to_winpath("c:foo"), L""); + ASSERT_EQ(testonly_utf8_to_winpath("c:/foo"), L"\\\\?\\c:\\foo"); + ASSERT_EQ(testonly_utf8_to_winpath("\\\\?\\C:\\foo"), L"\\\\?\\C:\\foo"); +} + +TEST_F(IoWin32Test, Utf8Utf16ConversionTest) { + string mbs; + wstring wcs; + ASSERT_TRUE(strings::utf8_to_wcs(kUtf8Text, &wcs)); + ASSERT_TRUE(strings::wcs_to_utf8(kUtf16Text, &mbs)); + ASSERT_EQ(wcs, kUtf16Text); + ASSERT_EQ(mbs, kUtf8Text); +} + +} // namespace +} // namespace win32 +} // namespace internal +} // namespace protobuf +} // namespace google + +#endif // defined(_WIN32) +