Skip to content

Commit

Permalink
Bazel client, Windows: implement MakeCanonical
Browse files Browse the repository at this point in the history
See #2107

--
Change-Id: I27a97881e3e19cbb7913e1248a24e9e631bc4f40
Reviewed-on: https://cr.bazel.build/8951
PiperOrigin-RevId: 147719277
MOS_MIGRATED_REVID=147719277
  • Loading branch information
laszlocsomor authored and dslomov committed Feb 16, 2017
1 parent 91deb23 commit 9b75b68
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 15 deletions.
2 changes: 0 additions & 2 deletions src/main/cpp/util/file_posix.cc
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,6 @@ bool UnlinkPath(const string &file_path) {
bool PathExists(const string& path) {
return access(path.c_str(), F_OK) == 0;
}
#endif // not __CYGWIN__

string MakeCanonical(const char *path) {
char *resolved_path = realpath(path, NULL);
Expand All @@ -237,7 +236,6 @@ string MakeCanonical(const char *path) {
}
}

#ifndef __CYGWIN__
static bool CanAccess(const string &path, bool read, bool write, bool exec) {
int mode = 0;
if (read) {
Expand Down
116 changes: 106 additions & 10 deletions src/main/cpp/util/file_windows.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "src/main/cpp/util/file_platform.h"

#include <ctype.h> // isalpha
#include <wchar.h> // wcslen
#include <wctype.h> // iswalpha
#include <windows.h>

Expand Down Expand Up @@ -307,15 +308,18 @@ static bool IsRootOrAbsolute(const basic_string<char_type>& path,
HasDriveSpecifierPrefix(path.c_str() + 4) && IsPathSeparator(path[6]));
}

pair<string, string> SplitPath(const string& path) {
template <typename char_type>
static pair<basic_string<char_type>, basic_string<char_type> > SplitPathImpl(
const basic_string<char_type>& path) {
if (path.empty()) {
return std::make_pair("", "");
return std::make_pair(basic_string<char_type>(), 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) && IsRootDirectory(path.substr(0, pos + 1))) {
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"
Expand All @@ -332,12 +336,21 @@ pair<string, string> SplitPath(const string& path) {
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 ? "" : path.substr(pos + 1));
pos == path.size() - 1 ? basic_string<char_type>()
: path.substr(pos + 1));
}
}
}
// Handle the case with no '/' or '\' in `path`.
return std::make_pair("", path);
return std::make_pair(basic_string<char_type>(), path);
}

pair<string, string> SplitPath(const string& path) {
return SplitPathImpl(path);
}

pair<wstring, wstring> SplitPathW(const wstring& path) {
return SplitPathImpl(path);
}

class MsysRoot {
Expand Down Expand Up @@ -780,13 +793,96 @@ bool PathExists(const string& path) {
return JunctionResolver().Resolve(wpath.c_str(), nullptr);
}

#ifdef COMPILER_MSVC
string MakeCanonical(const char* path) {
// TODO(bazel-team): implement this.
pdie(255, "blaze_util::MakeCanonical is not implemented on Windows");
return "";
if (IsDevNull(path)) {
return "NUL";
}
wstring wpath;
if (path == nullptr || path[0] == 0 ||
!AsWindowsPathWithUncPrefix(path, &wpath)) {
if (path != nullptr && path[0] != 0) {
PrintError("MakeCanonical(%s): AsWindowsPathWithUncPrefix failed", path);
}
return "";
}

// 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<wstring> realpath_reversed;
while (true) {
// Resolve the last segment.
unique_ptr<WCHAR[]> 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<wstring, wstring> 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 (windows_util::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<WCHAR[]> long_realpath;
if (!windows_util::GetLongPath(realpath.c_str(), &long_realpath)) {
return "";
}

// Convert the path to lower-case.
size_t size = wcslen(long_realpath.get()) -
(windows_util::HasUncPrefix(long_realpath.get()) ? 4 : 0);
unique_ptr<WCHAR[]> lcase_realpath(new WCHAR[size + 1]);
const WCHAR* p_from =
long_realpath.get() +
(windows_util::HasUncPrefix(long_realpath.get()) ? 4 : 0);
WCHAR* p_to = lcase_realpath.get();
while (size-- > 0) {
*p_to++ = towlower(*p_from++);
}
*p_to = 0;
return string(WstringToCstring(lcase_realpath.get()).get());
}
#endif // COMPILER_MSVC

static bool CanReadFileW(const wstring& path) {
DWORD attrs = ::GetFileAttributesW(path.c_str());
Expand Down
45 changes: 42 additions & 3 deletions src/test/cpp/util/file_windows_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ using std::wstring;

// Methods defined in file_windows.cc that are only visible for testing.
bool AsWindowsPath(const string& path, wstring* result);
bool AsWindowsPathWithUncPrefix(const string& path, wstring* wpath);
void ResetMsysRootForTesting();
string NormalizeWindowsPath(string path);

Expand Down Expand Up @@ -144,7 +145,7 @@ TEST_F(FileWindowsTest, TestBasename) {
ASSERT_EQ("foo", Basename("\\\\?\\c:\\foo"));
}

TEST_F(FileWindowsTest, IsAbsolute) {
TEST_F(FileWindowsTest, TestIsAbsolute) {
ASSERT_FALSE(IsAbsolute(""));
ASSERT_TRUE(IsAbsolute("/"));
ASSERT_TRUE(IsAbsolute("/foo"));
Expand All @@ -158,7 +159,7 @@ TEST_F(FileWindowsTest, IsAbsolute) {
ASSERT_TRUE(IsAbsolute("\\\\?\\c:\\foo"));
}

TEST_F(FileWindowsTest, IsRootDirectory) {
TEST_F(FileWindowsTest, TestIsRootDirectory) {
ASSERT_FALSE(IsRootDirectory(""));
ASSERT_TRUE(IsRootDirectory("/"));
ASSERT_FALSE(IsRootDirectory("/foo"));
Expand Down Expand Up @@ -403,7 +404,7 @@ TEST_F(FileWindowsTest, TestMakeDirectories) {
ASSERT_TRUE(MakeDirectories(string("\\\\?\\") + tmpdir + "/dir4/dir5", 0777));
}

TEST_F(FileWindowsTest, CanAccess) {
TEST_F(FileWindowsTest, TestCanAccess) {
ASSERT_FALSE(CanReadFile("C:/windows/this/should/not/exist/mkay"));
ASSERT_FALSE(CanExecuteFile("C:/this/should/not/exist/mkay"));
ASSERT_FALSE(CanAccessDirectory("C:/this/should/not/exist/mkay"));
Expand Down Expand Up @@ -448,4 +449,42 @@ TEST_F(FileWindowsTest, CanAccess) {
ASSERT_FALSE(CanAccessDirectory(file2));
}

TEST_F(FileWindowsTest, TestMakeCanonical) {
string tmpdir;
GET_TEST_TMPDIR(tmpdir);
// Create some scratch directories: $TEST_TMPDIR/directory/subdirectory
string dir1(JoinPath(tmpdir, "directory"));
string dir2(JoinPath(dir1, "subdirectory"));
EXPECT_TRUE(MakeDirectories(dir2, 0700));
// Create a dummy file: $TEST_TMPDIR/directory/subdirectory/foo.txt
string foo(JoinPath(dir2, "foo.txt"));
wstring wfoo;
EXPECT_TRUE(AsWindowsPathWithUncPrefix(foo, &wfoo));
EXPECT_TRUE(CreateDummyFile(wfoo));
EXPECT_TRUE(CanReadFile(foo));
// Create junctions next to directory and subdirectory, pointing to them.
// Use short paths and mixed casing to test that the canonicalization can
// resolve these.
// $TEST_TMPDIR/junc12345 -> $TEST_TMPDIR/DIRECT~1
// $TEST_TMPDIR/junc12~1/junc67890 -> $TEST_TMPDIR/JUNC12~1/SubDir~1
string sym1(JoinPath(tmpdir, "junc12345"));
string sym2(JoinPath(JoinPath(tmpdir, "junc12~1"), "junc67890"));
string sym1value(JoinPath(tmpdir, "DIRECT~1"));
string sym2value(JoinPath(JoinPath(tmpdir, "JUNC12~1"), "SubDir~1"));
CREATE_JUNCTION(sym1, sym1value);
CREATE_JUNCTION(sym2, sym2value);
// Expect that $TEST_TMPDIR/sym1/sym2/foo.txt is readable.
string symfoo(JoinPath(sym2, "foo.txt"));
EXPECT_TRUE(CanReadFile(symfoo));
// Assert the canonical path of foo.txt via the real path and via sym2.
// The latter contains at least two junction components, shortened paths, and
// mixed casing.
string dircanon(MakeCanonical(foo.c_str()));
string symcanon(MakeCanonical(symfoo.c_str()));
string expected("directory\\subdirectory\\foo.txt");
ASSERT_NE(symcanon, "");
ASSERT_EQ(symcanon.find(expected), symcanon.size() - expected.size());
ASSERT_EQ(dircanon, symcanon);
}

} // namespace blaze_util

0 comments on commit 9b75b68

Please sign in to comment.