Bazel client, Windows: support /dev/null

Support passing /dev/null and NUL as flag values
for flags like --bazelrc.

In particular, special-case these paths in
methods like blaze_util::ReadFile,
blaze_util::IsDirectory, etc.

Fixes https://github.com/bazelbuild/bazel/issues/2354

RELNOTES[NEW]: Windows: "/dev/null" is now a supported path, e.g. --bazelrc=/dev/null now works

--
PiperOrigin-RevId: 144195994
MOS_MIGRATED_REVID=144195994
diff --git a/src/main/cpp/util/file_windows.cc b/src/main/cpp/util/file_windows.cc
index 22ab715..63d6d66 100644
--- a/src/main/cpp/util/file_windows.cc
+++ b/src/main/cpp/util/file_windows.cc
@@ -38,6 +38,8 @@
 // The result may have a UNC prefix.
 static unique_ptr<WCHAR[]> GetCwdW();
 
+static bool IsDevNull(const string& path);
+
 // 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.
@@ -287,6 +289,10 @@
     result->clear();
     return true;
   }
+  if (IsDevNull(path)) {
+    result->assign(L"NUL");
+    return true;
+  }
 
   string mutable_path = path;
   if (path[0] == '/') {
@@ -318,6 +324,11 @@
 }
 
 static bool AsWindowsPathWithUncPrefix(const string& path, wstring* wpath) {
+  if (IsDevNull(path)) {
+    wpath->assign(L"NUL");
+    return true;
+  }
+
   if (!AsWindowsPath(path, wpath)) {
     PrintError("AsWindowsPathWithUncPrefix(%s): AsWindowsPath failed, err=%d\n",
                path.c_str(), GetLastError());
@@ -331,6 +342,11 @@
 }
 
 bool AsShortWindowsPath(const string& path, string* result) {
+  if (IsDevNull(path)) {
+    result->assign("NUL");
+    return true;
+  }
+
   result->clear();
   wstring wpath;
   if (!AsWindowsPathWithUncPrefix(path, &wpath)) {
@@ -359,6 +375,10 @@
   if (filename.empty()) {
     return false;
   }
+  if (IsDevNull(filename)) {
+    content->clear();
+    return true;
+  }
   wstring wfilename;
   if (!AsWindowsPathWithUncPrefix(filename, &wfilename)) {
     return false;
@@ -415,6 +435,10 @@
 }
 
 bool UnlinkPath(const string& file_path) {
+  if (IsDevNull(file_path)) {
+    return false;
+  }
+
   wstring wpath;
   if (!AsWindowsPathWithUncPrefix(file_path, &wpath)) {
     return false;
@@ -562,6 +586,9 @@
   if (path.empty()) {
     return false;
   }
+  if (IsDevNull(path)) {
+    return true;
+  }
   wstring wpath;
   if (!AsWindowsPathWithUncPrefix(path, &wpath)) {
     PrintError("PathExists(%s): AsWindowsPathWithUncPrefix failed, err=%d\n",
@@ -591,6 +618,10 @@
 }
 #endif  // COMPILER_MSVC
 
+static bool IsDevNull(const string& path) {
+  return path == "/dev/null" || AsLower(path) == "nul";
+}
+
 static bool IsDirectoryW(const wstring& path) {
   DWORD attrs = ::GetFileAttributesW(path.c_str());
   return (attrs != INVALID_FILE_ATTRIBUTES) &&
@@ -599,7 +630,7 @@
 }
 
 bool IsDirectory(const string& path) {
-  if (path.empty()) {
+  if (path.empty() || IsDevNull(path)) {
     return false;
   }
   wstring wpath;
diff --git a/src/main/cpp/util/strings.cc b/src/main/cpp/util/strings.cc
index 3fed561..383ffa1 100644
--- a/src/main/cpp/util/strings.cc
+++ b/src/main/cpp/util/strings.cc
@@ -299,15 +299,20 @@
 
 void ToLower(string *str) {
   assert(str);
-  if (str->empty()) {
-    return;
-  }
+  *str = AsLower(*str);
+}
 
-  string temp = "";
-  for (auto ch : *str) {
-    temp += tolower(ch);
+string AsLower(const string &str) {
+  if (str.empty()) {
+    return "";
   }
-  *str = temp;
+  unique_ptr<char[]> result(new char[str.size() + 1]);
+  char *result_ptr = result.get();
+  for (const auto &ch : str) {
+    *result_ptr++ = tolower(ch);
+  }
+  result.get()[str.size()] = 0;
+  return string(result.get());
 }
 
 template <typename U, typename V>
diff --git a/src/main/cpp/util/strings.h b/src/main/cpp/util/strings.h
index 6650226..61187b7 100644
--- a/src/main/cpp/util/strings.h
+++ b/src/main/cpp/util/strings.h
@@ -106,6 +106,8 @@
 // Convert str to lower case. No locale handling, this is just for ASCII.
 void ToLower(std::string *str);
 
+std::string AsLower(const std::string &str);
+
 // Convert a wchar_t string to a char string. Useful when consuming results of
 // widechar Windows API functions.
 std::unique_ptr<char[]> WstringToCstring(const wchar_t *input);
diff --git a/src/test/cpp/util/file_test.cc b/src/test/cpp/util/file_test.cc
index aa654cc..52d7f8d 100644
--- a/src/test/cpp/util/file_test.cc
+++ b/src/test/cpp/util/file_test.cc
@@ -70,6 +70,9 @@
 
   ASSERT_TRUE(ReadFile(filename, &actual, 5));
   ASSERT_EQ(std::string("hello"), actual);
+
+  ASSERT_TRUE(ReadFile("/dev/null", &actual, 42));
+  ASSERT_EQ(std::string(""), actual);
 }
 
 }  // namespace blaze_util
diff --git a/src/test/cpp/util/file_windows_test.cc b/src/test/cpp/util/file_windows_test.cc
index 9546055..49c69bc 100644
--- a/src/test/cpp/util/file_windows_test.cc
+++ b/src/test/cpp/util/file_windows_test.cc
@@ -151,6 +151,12 @@
   ASSERT_TRUE(AsWindowsPath("\\\\?\\c://../foo", &actual));
   ASSERT_EQ(wstring(L"c:\\foo"), actual);
 
+  ASSERT_TRUE(AsWindowsPath("/dev/null", &actual));
+  ASSERT_EQ(wstring(L"NUL"), actual);
+
+  ASSERT_TRUE(AsWindowsPath("Nul", &actual));
+  ASSERT_EQ(wstring(L"NUL"), actual);
+
   ASSERT_TRUE(AsWindowsPath("/c", &actual));
   ASSERT_EQ(wstring(L"c:\\"), actual);
 
@@ -179,6 +185,13 @@
 }
 
 TEST(FileTest, TestAsShortWindowsPath) {
+  string actual;
+  ASSERT_TRUE(AsShortWindowsPath("/dev/null", &actual));
+  ASSERT_EQ(string("NUL"), actual);
+
+  ASSERT_TRUE(AsShortWindowsPath("nul", &actual));
+  ASSERT_EQ(string("NUL"), actual);
+
   string tmpdir(GetTestTmpDir());
   ASSERT_LT(0, tmpdir.size());
 
@@ -191,7 +204,6 @@
   ASSERT_EQ(0, mkdir(dirname.c_str()));
   ASSERT_TRUE(PathExists(dirname));
 
-  string actual;
   ASSERT_TRUE(AsShortWindowsPath(dirname, &actual));
   ASSERT_EQ(short_tmpdir + "\\longpa~1", actual);
   ASSERT_EQ(0, rmdir(dirname.c_str()));
@@ -251,6 +263,8 @@
   ASSERT_FALSE(PathExists(""));
   ASSERT_TRUE(PathExists("."));
   ASSERT_FALSE(PathExists("non.existent"));
+  ASSERT_TRUE(PathExists("/dev/null"));
+  ASSERT_TRUE(PathExists("Nul"));
 
   string tmpdir(GetTestTmpDir());
   ASSERT_LT(0, tmpdir.size());
@@ -292,6 +306,8 @@
 
 TEST(FileTest, TestIsDirectory) {
   ASSERT_FALSE(IsDirectory(""));
+  ASSERT_FALSE(IsDirectory("/dev/null"));
+  ASSERT_FALSE(IsDirectory("Nul"));
 
   string tmpdir(GetTestTmpDir());
   ASSERT_LT(0, tmpdir.size());
@@ -327,6 +343,9 @@
 }
 
 TEST(FileTest, TestUnlinkPath) {
+  ASSERT_FALSE(UnlinkPath("/dev/null"));
+  ASSERT_FALSE(UnlinkPath("Nul"));
+
   string tmpdir(GetTestTmpDir());
   ASSERT_LT(0, tmpdir.size());
   ASSERT_TRUE(PathExists(tmpdir));