Client: introduce Path class, convert server_dir

Benefits of a Path abstraction instead of raw
strings:

- type safety
- no need to convert paths on Windows all the time
- no need to check if paths are absolute

Closes #9232.

Change-Id: Id81da64c2d9c2881e0a57c6d886d7e2e62ecb8af
PiperOrigin-RevId: 265455526
diff --git a/src/main/cpp/blaze.cc b/src/main/cpp/blaze.cc
index 2b80b82..c88e136 100644
--- a/src/main/cpp/blaze.cc
+++ b/src/main/cpp/blaze.cc
@@ -622,11 +622,11 @@
   return result;
 }
 
-static void EnsureServerDir(const string &server_dir) {
+static void EnsureServerDir(const blaze_util::Path &server_dir) {
   // The server dir has the connection info - don't allow access by other users.
   if (!blaze_util::MakeDirectories(server_dir, 0700)) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "server directory '" << server_dir
+        << "server directory '" << server_dir.AsPrintablePath()
         << "' could not be created: " << GetLastErrorString();
   }
 }
@@ -644,13 +644,10 @@
 
 // Replace this process with the blaze server. Does not exit.
 static void RunServerMode(
-    const string &server_exe,
-    const vector<string> &server_exe_args,
-    const WorkspaceLayout &workspace_layout,
-    const string &workspace,
-    const OptionProcessor &option_processor,
-    const StartupOptions &startup_options,
-    BlazeServer *server) {
+    const string &server_exe, const vector<string> &server_exe_args,
+    const blaze_util::Path &server_dir, const WorkspaceLayout &workspace_layout,
+    const string &workspace, const OptionProcessor &option_processor,
+    const StartupOptions &startup_options, BlazeServer *server) {
   if (startup_options.batch) {
     BAZEL_DIE(blaze_exit_code::BAD_ARGV)
         << "exec-server command is not compatible with --batch";
@@ -663,15 +660,12 @@
     server->KillRunningServer();
   }
 
-  const string server_dir =
-      blaze_util::JoinPath(startup_options.output_base, "server");
-
   EnsureServerDir(server_dir);
 
   blaze_util::WriteFile(blaze::GetProcessIdAsString(),
-                        blaze_util::JoinPath(server_dir, "server.pid.txt"));
+                        server_dir.GetRelative("server.pid.txt"));
   blaze_util::WriteFile(GetArgumentString(server_exe_args),
-                        blaze_util::JoinPath(server_dir, "cmdline"));
+                        server_dir.GetRelative("cmdline"));
 
   GoToWorkspace(workspace_layout, workspace);
 
@@ -768,10 +762,10 @@
 
 // After connecting to the Blaze server, return its PID, or -1 if there was an
 // error.
-static int GetServerPid(const string &server_dir) {
+static int GetServerPid(const blaze_util::Path &server_dir) {
   // Note: there is no race here on startup since the server creates
   // the pid file strictly before it binds the socket.
-  string pid_file = blaze_util::JoinPath(server_dir, kServerPidFile);
+  blaze_util::Path pid_file = server_dir.GetRelative(kServerPidFile);
   string bufstr;
   int result;
   if (!blaze_util::ReadFile(pid_file, &bufstr, 32) ||
@@ -838,7 +832,7 @@
 // Ensures that any server previously associated with `server_dir` is no longer
 // running.
 static void EnsurePreviousServerProcessTerminated(
-    const string &server_dir, const StartupOptions &startup_options,
+    const blaze_util::Path &server_dir, const StartupOptions &startup_options,
     LoggingInfo *logging_info) {
   int server_pid = GetServerPid(server_dir);
   if (server_pid > 0) {
@@ -858,23 +852,16 @@
 
 // Starts up a new server and connects to it. Exits if it didn't work out.
 static void StartServerAndConnect(
-    const string &server_exe,
-    const vector<string> &server_exe_args,
-    const WorkspaceLayout &workspace_layout,
-    const string &workspace,
-    const OptionProcessor &option_processor,
-    const StartupOptions &startup_options,
-    LoggingInfo *logging_info,
+    const string &server_exe, const vector<string> &server_exe_args,
+    const blaze_util::Path &server_dir, const WorkspaceLayout &workspace_layout,
+    const string &workspace, const OptionProcessor &option_processor,
+    const StartupOptions &startup_options, LoggingInfo *logging_info,
     BlazeServer *server) {
-  const string server_dir =
-      blaze_util::JoinPath(startup_options.output_base, "server");
-
   // Delete the old command_port file if it already exists. Otherwise we might
   // run into the race condition that we read the old command_port file before
   // the new server has written the new file and we try to connect to the old
   // port, run into a timeout and try again.
-  (void)blaze_util::UnlinkPath(
-      blaze_util::JoinPath(server_dir, "command_port"));
+  (void)blaze_util::UnlinkPath(server_dir.GetRelative("command_port"));
 
   EnsureServerDir(server_dir);
 
@@ -886,7 +873,7 @@
   // cmdline file is used to validate the server running in this server_dir.
   // There's no server running now so we're safe to unconditionally write this.
   blaze_util::WriteFile(GetArgumentString(server_exe_args),
-                        blaze_util::JoinPath(server_dir, "cmdline"));
+                        server_dir.GetRelative("cmdline"));
 
   // Do this here instead of in the daemon so the user can see if it fails.
   GoToWorkspace(workspace_layout, workspace);
@@ -1217,22 +1204,16 @@
 // signal.
 static ATTRIBUTE_NORETURN void RunClientServerMode(
     const string &server_exe, const vector<string> &server_exe_args,
-    const WorkspaceLayout &workspace_layout, const string &workspace,
-    const OptionProcessor &option_processor,
+    const blaze_util::Path &server_dir, const WorkspaceLayout &workspace_layout,
+    const string &workspace, const OptionProcessor &option_processor,
     const StartupOptions &startup_options, LoggingInfo *logging_info,
     const DurationMillis extract_data_duration,
     const DurationMillis command_wait_duration_ms, BlazeServer *server) {
   while (true) {
     if (!server->Connected()) {
-      StartServerAndConnect(
-          server_exe,
-          server_exe_args,
-          workspace_layout,
-          workspace,
-          option_processor,
-          startup_options,
-          logging_info,
-          server);
+      StartServerAndConnect(server_exe, server_exe_args, server_dir,
+                            workspace_layout, workspace, option_processor,
+                            startup_options, logging_info, server);
     }
 
     // Check for the case when the workspace directory deleted and then gets
@@ -1520,23 +1501,19 @@
 
   const string server_exe = startup_options.GetExe(jvm_path, server_jar_path);
 
+  const blaze_util::Path server_dir =
+      blaze_util::Path(startup_options.output_base).GetRelative("server");
   if ("exec-server" == option_processor.GetCommand()) {
-    RunServerMode(
-        server_exe,
-        server_exe_args,
-        workspace_layout,
-        workspace,
-        option_processor,
-        startup_options,
-        blaze_server);
+    RunServerMode(server_exe, server_exe_args, server_dir, workspace_layout,
+                  workspace, option_processor, startup_options, blaze_server);
   } else if (startup_options.batch) {
     RunBatchMode(server_exe, server_exe_args, workspace_layout, workspace,
                  option_processor, startup_options, logging_info,
                  extract_data_duration, command_wait_duration_ms, blaze_server);
   } else {
-    RunClientServerMode(server_exe, server_exe_args, workspace_layout,
-                        workspace, option_processor, startup_options,
-                        logging_info, extract_data_duration,
+    RunClientServerMode(server_exe, server_exe_args, server_dir,
+                        workspace_layout, workspace, option_processor,
+                        startup_options, logging_info, extract_data_duration,
                         command_wait_duration_ms, blaze_server);
   }
 }
@@ -1708,7 +1685,7 @@
     return false;
   }
 
-  pid_t server_pid = GetServerPid(server_dir);
+  pid_t server_pid = GetServerPid(blaze_util::Path(server_dir));
   if (server_pid < 0) {
     return false;
   }
diff --git a/src/main/cpp/blaze_util_darwin.cc b/src/main/cpp/blaze_util_darwin.cc
index 699407e..fbeb0a1 100644
--- a/src/main/cpp/blaze_util_darwin.cc
+++ b/src/main/cpp/blaze_util_darwin.cc
@@ -201,9 +201,8 @@
   return posix_spawnattr_set_qos_class_np(attrp, options.macos_qos_class);
 }
 
-void WriteSystemSpecificProcessIdentifier(
-    const string& server_dir, pid_t server_pid) {
-}
+void WriteSystemSpecificProcessIdentifier(const blaze_util::Path &server_dir,
+                                          pid_t server_pid) {}
 
 bool VerifyServerProcess(int pid, const string &output_base) {
   // TODO(lberki): This only checks for the process's existence, not whether
diff --git a/src/main/cpp/blaze_util_freebsd.cc b/src/main/cpp/blaze_util_freebsd.cc
index 9d873d0..b731d38 100644
--- a/src/main/cpp/blaze_util_freebsd.cc
+++ b/src/main/cpp/blaze_util_freebsd.cc
@@ -161,9 +161,8 @@
   return 0;
 }
 
-void WriteSystemSpecificProcessIdentifier(
-    const string& server_dir, pid_t server_pid) {
-}
+void WriteSystemSpecificProcessIdentifier(const blaze_util::Path &server_dir,
+                                          pid_t server_pid) {}
 
 bool VerifyServerProcess(int pid, const string &output_base) {
   // TODO(lberki): This only checks for the process's existence, not whether
diff --git a/src/main/cpp/blaze_util_linux.cc b/src/main/cpp/blaze_util_linux.cc
index 731f478..b23898e 100644
--- a/src/main/cpp/blaze_util_linux.cc
+++ b/src/main/cpp/blaze_util_linux.cc
@@ -210,8 +210,8 @@
   return 0;
 }
 
-void WriteSystemSpecificProcessIdentifier(
-    const string& server_dir, pid_t server_pid) {
+void WriteSystemSpecificProcessIdentifier(const blaze_util::Path &server_dir,
+                                          pid_t server_pid) {
   string pid_string = ToString(server_pid);
 
   string start_time;
@@ -221,11 +221,11 @@
         << GetLastErrorString();
   }
 
-  string start_time_file = blaze_util::JoinPath(server_dir, "server.starttime");
+  blaze_util::Path start_time_file = server_dir.GetRelative("server.starttime");
   if (!blaze_util::WriteFile(start_time, start_time_file)) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "Cannot write start time in server dir " << server_dir << ": "
-        << GetLastErrorString();
+        << "Cannot write start time in server dir "
+        << server_dir.AsPrintablePath() << ": " << GetLastErrorString();
   }
 }
 
diff --git a/src/main/cpp/blaze_util_platform.h b/src/main/cpp/blaze_util_platform.h
index c7c3aee..d001306 100644
--- a/src/main/cpp/blaze_util_platform.h
+++ b/src/main/cpp/blaze_util_platform.h
@@ -22,6 +22,7 @@
 
 #include "src/main/cpp/blaze_util.h"
 #include "src/main/cpp/server_process_info.h"
+#include "src/main/cpp/util/path.h"
 #include "src/main/cpp/util/port.h"
 
 namespace blaze {
@@ -171,15 +172,12 @@
 // still alive. The PID of the daemon started is written into server_dir,
 // both as a symlink (for legacy reasons) and as a file, and returned to the
 // caller.
-int ExecuteDaemon(const std::string& exe,
-                  const std::vector<std::string>& args_vector,
-                  const std::map<std::string, EnvVarValue>& env,
-                  const std::string& daemon_output,
-                  const bool daemon_output_append,
-                  const std::string& binaries_dir,
-                  const std::string& server_dir,
-                  const StartupOptions &options,
-                  BlazeServerStartup** server_startup);
+int ExecuteDaemon(
+    const std::string& exe, const std::vector<std::string>& args_vector,
+    const std::map<std::string, EnvVarValue>& env,
+    const std::string& daemon_output, const bool daemon_output_append,
+    const std::string& binaries_dir, const blaze_util::Path& server_dir,
+    const StartupOptions& options, BlazeServerStartup** server_startup);
 
 // A character used to separate paths in a list.
 extern const char kListSeparator;
diff --git a/src/main/cpp/blaze_util_posix.cc b/src/main/cpp/blaze_util_posix.cc
index 87ba899..23c2c8c 100644
--- a/src/main/cpp/blaze_util_posix.cc
+++ b/src/main/cpp/blaze_util_posix.cc
@@ -352,23 +352,21 @@
 int ConfigureDaemonProcess(posix_spawnattr_t* attrp,
                            const StartupOptions &options);
 
-void WriteSystemSpecificProcessIdentifier(
-    const string& server_dir, pid_t server_pid);
+void WriteSystemSpecificProcessIdentifier(const blaze_util::Path& server_dir,
+                                          pid_t server_pid);
 
-int ExecuteDaemon(const string& exe,
-                  const std::vector<string>& args_vector,
+int ExecuteDaemon(const string& exe, const std::vector<string>& args_vector,
                   const std::map<string, EnvVarValue>& env,
-                  const string& daemon_output,
-                  const bool daemon_output_append,
+                  const string& daemon_output, const bool daemon_output_append,
                   const string& binaries_dir,
-                  const string& server_dir,
-                  const StartupOptions &options,
+                  const blaze_util::Path& server_dir,
+                  const StartupOptions& options,
                   BlazeServerStartup** server_startup) {
-  const string pid_file = blaze_util::JoinPath(server_dir, kServerPidFile);
+  const blaze_util::Path pid_file = server_dir.GetRelative(kServerPidFile);
   const string daemonize = blaze_util::JoinPath(binaries_dir, "daemonize");
 
-  std::vector<string> daemonize_args =
-      {"daemonize", "-l", daemon_output, "-p", pid_file};
+  std::vector<string> daemonize_args = {"daemonize", "-l", daemon_output, "-p",
+                                        pid_file.AsNativePath()};
   if (daemon_output_append) {
     daemonize_args.push_back("-a");
   }
@@ -427,10 +425,11 @@
         << "daemonize didn't exit cleanly: " << GetLastErrorString();
   }
 
-  std::ifstream pid_reader(pid_file);
+  std::ifstream pid_reader(pid_file.AsNativePath());
   if (!pid_reader) {
+    string err = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::INTERNAL_ERROR)
-      << "Failed to open " << pid_file << ": " << GetLastErrorString();
+        << "Failed to open " << pid_file.AsPrintablePath() << ": " << err;
   }
   pid_t server_pid;
   pid_reader >> server_pid;
diff --git a/src/main/cpp/blaze_util_windows.cc b/src/main/cpp/blaze_util_windows.cc
index cc14a4a..dae4855 100644
--- a/src/main/cpp/blaze_util_windows.cc
+++ b/src/main/cpp/blaze_util_windows.cc
@@ -537,19 +537,21 @@
   return true;
 }
 
-static void WriteProcessStartupTime(const string& server_dir, HANDLE process) {
+static void WriteProcessStartupTime(const blaze_util::Path& server_dir,
+                                    HANDLE process) {
   uint64_t start_time = 0;
   if (!GetProcessStartupTime(process, &start_time)) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "WriteProcessStartupTime(" << server_dir
+        << "WriteProcessStartupTime(" << server_dir.AsPrintablePath()
         << "): GetProcessStartupTime failed: " << GetLastErrorString();
   }
 
-  string start_time_file = blaze_util::JoinPath(server_dir, "server.starttime");
+  blaze_util::Path start_time_file = server_dir.GetRelative("server.starttime");
   if (!blaze_util::WriteFile(ToString(start_time), start_time_file)) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "WriteProcessStartupTime(" << server_dir << "): WriteFile("
-        << start_time_file << ") failed: " << GetLastErrorString();
+        << "WriteProcessStartupTime(" << server_dir.AsPrintablePath()
+        << "): WriteFile(" << start_time_file.AsPrintablePath()
+        << ") failed: " << GetLastErrorString();
   }
 }
 
@@ -612,14 +614,12 @@
   AutoHandle proc;
 };
 
-int ExecuteDaemon(const string& exe,
-                  const std::vector<string>& args_vector,
+int ExecuteDaemon(const string& exe, const std::vector<string>& args_vector,
                   const std::map<string, EnvVarValue>& env,
-                  const string& daemon_output,
-                  const bool daemon_out_append,
+                  const string& daemon_output, const bool daemon_out_append,
                   const string& binaries_dir,
-                  const string& server_dir,
-                  const StartupOptions &options,
+                  const blaze_util::Path& server_dir,
+                  const StartupOptions& options,
                   BlazeServerStartup** server_startup) {
   wstring wdaemon_output;
   string error;
@@ -725,10 +725,11 @@
   *server_startup = new ProcessHandleBlazeServerStartup(processInfo.hProcess);
 
   string pid_string = ToString(processInfo.dwProcessId);
-  string pid_file = blaze_util::JoinPath(server_dir, kServerPidFile);
+  blaze_util::Path pid_file = server_dir.GetRelative(kServerPidFile);
   if (!blaze_util::WriteFile(pid_string, pid_file)) {
     // Not a lot we can do if this fails
-    fprintf(stderr, "Cannot write PID file %s\n", pid_file.c_str());
+    fprintf(stderr, "Cannot write PID file %s\n",
+            pid_file.AsPrintablePath().c_str());
   }
 
   // Don't close processInfo.hProcess here, it's now owned by the
diff --git a/src/main/cpp/util/file.cc b/src/main/cpp/util/file.cc
index 041d779..87f2f02 100644
--- a/src/main/cpp/util/file.cc
+++ b/src/main/cpp/util/file.cc
@@ -88,6 +88,11 @@
   return WriteFile(content.c_str(), content.size(), filename, perm);
 }
 
+bool WriteFile(const std::string &content, const Path &path,
+               unsigned int perm) {
+  return WriteFile(content.c_str(), content.size(), path, perm);
+}
+
 class DirectoryTreeWalker : public DirectoryEntryConsumer {
  public:
   DirectoryTreeWalker(vector<string> *files,
diff --git a/src/main/cpp/util/file.h b/src/main/cpp/util/file.h
index 235ec87..be9917b 100644
--- a/src/main/cpp/util/file.h
+++ b/src/main/cpp/util/file.h
@@ -63,6 +63,9 @@
 bool WriteFile(const std::string &content, const std::string &filename,
                unsigned int perm = 0644);
 
+bool WriteFile(const std::string &content, const Path &path,
+               unsigned int perm = 0644);
+
 // Lists all files in `path` and all of its subdirectories.
 //
 // Does not follow symlinks / junctions.
diff --git a/src/main/cpp/util/file_platform.h b/src/main/cpp/util/file_platform.h
index 7457e5e..25dc856 100644
--- a/src/main/cpp/util/file_platform.h
+++ b/src/main/cpp/util/file_platform.h
@@ -23,6 +23,8 @@
 
 namespace blaze_util {
 
+class Path;
+
 class IPipe;
 
 IPipe* CreatePipe();
@@ -91,17 +93,22 @@
 // Returns false on error. Can be called from a signal handler.
 bool ReadFile(const std::string &filename, std::string *content,
               int max_size = 0);
+bool ReadFile(const Path &path, std::string *content, int max_size = 0);
 
 // Reads up to `size` bytes from the file `filename` into `data`.
 // There must be enough memory allocated at `data`.
 // Returns true on success, false on error.
 bool ReadFile(const std::string &filename, void *data, size_t size);
+bool ReadFile(const Path &filename, void *data, size_t size);
 
 // Writes `size` bytes from `data` into file `filename` and chmods it to `perm`.
 // Returns false on failure, sets errno.
 bool WriteFile(const void *data, size_t size, const std::string &filename,
                unsigned int perm = 0644);
 
+bool WriteFile(const void *data, size_t size, const Path &path,
+               unsigned int perm = 0644);
+
 // Result of a `WriteToStdOutErr` operation.
 //
 // This is a platform-independent abstraction of `errno`. If you need to handle
@@ -141,6 +148,7 @@
 // Unlinks the file given by 'file_path'.
 // Returns true on success. In case of failure sets errno.
 bool UnlinkPath(const std::string &file_path);
+bool UnlinkPath(const Path &file_path);
 
 // Returns true if this path exists, following symlinks.
 bool PathExists(const std::string& path);
@@ -176,6 +184,7 @@
 // `mode` should be an octal permission mask, e.g. 0755.
 // Returns false on failure, sets errno.
 bool MakeDirectories(const std::string &path, unsigned int mode);
+bool MakeDirectories(const Path &path, unsigned int mode);
 
 // Returns the current working directory.
 // The path is platform-specific (e.g. Windows path of Windows) and absolute.
diff --git a/src/main/cpp/util/file_posix.cc b/src/main/cpp/util/file_posix.cc
index 8032e80..3133345 100644
--- a/src/main/cpp/util/file_posix.cc
+++ b/src/main/cpp/util/file_posix.cc
@@ -207,6 +207,10 @@
   return result;
 }
 
+bool ReadFile(const Path &path, std::string *content, int max_size) {
+  return ReadFile(path.AsNativePath(), content, max_size);
+}
+
 bool ReadFile(const string &filename, void *data, size_t size) {
   int fd = open(filename.c_str(), O_RDONLY);
   if (fd == -1) return false;
@@ -215,6 +219,10 @@
   return result;
 }
 
+bool ReadFile(const Path &filename, void *data, size_t size) {
+  return ReadFile(filename.AsNativePath(), data, size);
+}
+
 bool WriteFile(const void *data, size_t size, const string &filename,
                unsigned int perm) {
   UnlinkPath(filename);  // We don't care about the success of this.
@@ -229,6 +237,11 @@
   return result == static_cast<int>(size);
 }
 
+bool WriteFile(const void *data, size_t size, const Path &path,
+               unsigned int perm) {
+  return WriteFile(data, size, path.AsNativePath(), perm);
+}
+
 int WriteToStdOutErr(const void *data, size_t size, bool to_stdout) {
   size_t r = fwrite(data, 1, size, to_stdout ? stdout : stderr);
   return (r == size) ? WriteResult::SUCCESS
@@ -264,6 +277,10 @@
   return unlink(file_path.c_str()) == 0;
 }
 
+bool UnlinkPath(const Path &file_path) {
+  return UnlinkPath(file_path.AsNativePath());
+}
+
 bool PathExists(const string& path) {
   return access(path.c_str(), F_OK) == 0;
 }
@@ -399,6 +416,10 @@
   return MakeDirectories(path, mode, true);
 }
 
+bool MakeDirectories(const Path &path, unsigned int mode) {
+  return MakeDirectories(path.AsNativePath(), mode);
+}
+
 string GetCwd() {
   char cwdbuf[PATH_MAX];
   if (getcwd(cwdbuf, sizeof cwdbuf) == NULL) {
diff --git a/src/main/cpp/util/file_windows.cc b/src/main/cpp/util/file_windows.cc
index b47e222..9a387bd 100644
--- a/src/main/cpp/util/file_windows.cc
+++ b/src/main/cpp/util/file_windows.cc
@@ -247,23 +247,9 @@
 
 IFileMtime* CreateFileMtime() { return new WindowsFileMtime(); }
 
-static bool OpenFileForReading(const string& filename, HANDLE* result) {
-  if (filename.empty()) {
-    return false;
-  }
-  // TODO(laszlocsomor): remove the following check; it won't allow opening NUL.
-  if (IsDevNull(filename.c_str())) {
-    return true;
-  }
-  wstring wfilename;
-  string error;
-  if (!AsAbsoluteWindowsPath(filename, &wfilename, &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "OpenFileForReading(" << filename
-        << "): AsAbsoluteWindowsPath failed: " << error;
-  }
+static bool OpenFileForReading(const Path& path, HANDLE* result) {
   *result = ::CreateFileW(
-      /* lpFileName */ wfilename.c_str(),
+      /* lpFileName */ path.AsNativePath().c_str(),
       /* dwDesiredAccess */ GENERIC_READ,
       /* dwShareMode */ kAllShare,
       /* lpSecurityAttributes */ NULL,
@@ -291,8 +277,20 @@
     content->clear();
     return true;
   }
+  return ReadFile(Path(filename), content, max_size);
+}
+
+bool ReadFile(const Path& path, std::string* content, int max_size) {
+  if (path.IsEmpty()) {
+    return false;
+  }
+  // TODO(laszlocsomor): remove the following check; it won't allow opening NUL.
+  if (path.IsNull()) {
+    return true;
+  }
+
   HANDLE handle;
-  if (!OpenFileForReading(filename, &handle)) {
+  if (!OpenFileForReading(path, &handle)) {
     return false;
   }
 
@@ -305,12 +303,19 @@
 }
 
 bool ReadFile(const string& filename, void* data, size_t size) {
-  if (IsDevNull(filename.c_str())) {
+  return ReadFile(Path(filename), data, size);
+}
+
+bool ReadFile(const Path& path, void* data, size_t size) {
+  if (path.IsEmpty()) {
+    return false;
+  }
+  if (path.IsNull()) {
     // mimic read(2) behavior: we can always read 0 bytes from /dev/null
     return true;
   }
   HANDLE handle;
-  if (!OpenFileForReading(filename, &handle)) {
+  if (!OpenFileForReading(path, &handle)) {
     return false;
   }
 
@@ -326,18 +331,14 @@
   if (IsDevNull(filename.c_str())) {
     return true;  // mimic write(2) behavior with /dev/null
   }
-  wstring wpath;
-  string error;
-  if (!AsAbsoluteWindowsPath(filename, &wpath, &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "WriteFile(" << filename
-        << "): AsAbsoluteWindowsPath failed: " << error;
-    return false;
-  }
+  return WriteFile(data, size, Path(filename), perm);
+}
 
-  UnlinkPathW(wpath);  // We don't care about the success of this.
+bool WriteFile(const void* data, size_t size, const Path& path,
+               unsigned int perm) {
+  UnlinkPathW(path.AsNativePath());  // We don't care about the success of this.
   AutoHandle handle(::CreateFileW(
-      /* lpFileName */ wpath.c_str(),
+      /* lpFileName */ path.AsNativePath().c_str(),
       /* dwDesiredAccess */ GENERIC_WRITE,
       /* dwShareMode */ FILE_SHARE_READ,
       /* lpSecurityAttributes */ NULL,
@@ -420,18 +421,11 @@
   if (IsDevNull(file_path.c_str())) {
     return false;
   }
-
-  wstring wpath;
-  string error;
-  if (!AsAbsoluteWindowsPath(file_path, &wpath, &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "UnlinkPath(" << file_path
-        << "): AsAbsoluteWindowsPath failed: " << error;
-    return false;
-  }
-  return UnlinkPathW(wpath);
+  return UnlinkPath(Path(file_path));
 }
 
+bool UnlinkPath(const Path& path) { return UnlinkPathW(path.AsNativePath()); }
+
 static bool RealPath(const WCHAR* path, unique_ptr<WCHAR[]>* result = nullptr) {
   // Attempt opening the path, which may be anything -- a file, a directory, a
   // symlink, even a dangling symlink is fine.
@@ -694,6 +688,10 @@
   return MakeDirectoriesW(wpath, mode);
 }
 
+bool MakeDirectories(const Path& path, unsigned int mode) {
+  return MakeDirectoriesW(path.AsNativePath(), mode);
+}
+
 static inline void ToLowerW(WCHAR* p) {
   while (*p) {
     *p++ = towlower(*p);
diff --git a/src/main/cpp/util/path_platform.h b/src/main/cpp/util/path_platform.h
index da5bf4b..27656f7 100644
--- a/src/main/cpp/util/path_platform.h
+++ b/src/main/cpp/util/path_platform.h
@@ -18,6 +18,31 @@
 
 namespace blaze_util {
 
+// Platform-native, absolute, normalized path.
+class Path {
+ public:
+  Path() {}
+  explicit Path(const std::string &path);
+  bool IsEmpty() const { return path_.empty(); }
+  bool IsNull() const;
+  Path GetRelative(const std::string &r) const;
+  std::string AsPrintablePath() const;
+
+#if defined(_WIN32) || defined(__CYGWIN__)
+  const std::wstring &AsNativePath() const { return path_; }
+#else
+  const std::string &AsNativePath() const { return path_; }
+#endif
+
+ private:
+#if defined(_WIN32) || defined(__CYGWIN__)
+  explicit Path(const std::wstring &wpath) : path_(wpath) {}
+  std::wstring path_;
+#else
+  std::string path_;
+#endif
+};
+
 // Convert a path from Bazel internal form to underlying OS form.
 // On Unixes this is an identity operation.
 // On Windows, Bazel internal form is cygwin path, and underlying OS form
@@ -138,6 +163,10 @@
 bool AsShortWindowsPath(const std::string &path, std::string *result,
                         std::string *error);
 
+#else
+
+std::string TestOnly_NormalizeAbsPath(const std::string &s);
+
 #endif  // defined(_WIN32) || defined(__CYGWIN__)
 }  // namespace blaze_util
 
diff --git a/src/main/cpp/util/path_posix.cc b/src/main/cpp/util/path_posix.cc
index 11577c3..eed7f64 100644
--- a/src/main/cpp/util/path_posix.cc
+++ b/src/main/cpp/util/path_posix.cc
@@ -94,4 +94,55 @@
   return MakeAbsolute(ResolveEnvvars(path));
 }
 
+static std::string NormalizeAbsPath(const std::string &p) {
+  if (p.empty() || p[0] != '/') {
+    return "";
+  }
+  typedef std::string::size_type index;
+  std::vector<std::pair<index, index> > segments;
+  for (index s = 0; s < p.size();) {
+    index e = p.find_first_of('/', s);
+    if (e == std::string::npos) {
+      e = p.size();
+    }
+    if (e > s) {
+      if (p.compare(s, e - s, "..") == 0) {
+        if (!segments.empty()) {
+          segments.pop_back();
+        }
+      } else if (p.compare(s, e - s, ".") != 0) {
+        segments.push_back(std::make_pair(s, e - s));
+      }
+    }
+    s = e + 1;
+  }
+  if (segments.empty()) {
+    return "/";
+  } else {
+    std::stringstream r;
+    for (const auto &s : segments) {
+      r << "/" << p.substr(s.first, s.second);
+    }
+    if (p[p.size() - 1] == '/') {
+      r << "/";
+    }
+    return r.str();
+  }
+}
+
+std::string TestOnly_NormalizeAbsPath(const std::string &s) {
+  return NormalizeAbsPath(s);
+}
+
+Path::Path(const std::string &path)
+    : path_(NormalizeAbsPath(MakeAbsolute(path))) {}
+
+bool Path::IsNull() const { return path_ == "/dev/null"; }
+
+Path Path::GetRelative(const std::string &r) const {
+  return Path(JoinPath(path_, r));
+}
+
+std::string Path::AsPrintablePath() const { return path_; }
+
 }  // namespace blaze_util
diff --git a/src/main/cpp/util/path_windows.cc b/src/main/cpp/util/path_windows.cc
index bb4da57..8621dff 100644
--- a/src/main/cpp/util/path_windows.cc
+++ b/src/main/cpp/util/path_windows.cc
@@ -455,4 +455,45 @@
   return 'a' + wdrive - offset;
 }
 
+Path::Path(const std::string& path) {
+  if (path.empty()) {
+    return;
+  } else if (IsDevNull(path.c_str())) {
+    path_ = L"NUL";
+  } else {
+    std::string error;
+    if (!AsAbsoluteWindowsPath(path, &path_, &error)) {
+      BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
+          << "Path::Path(" << path
+          << "): AsAbsoluteWindowsPath failed: " << error;
+    }
+  }
+}
+
+Path Path::GetRelative(const std::string& r) const {
+  if (r.empty()) {
+    return *this;
+  } else if (IsDevNull(r.c_str())) {
+    return Path(L"NUL");
+  } else if (IsAbsolute(r)) {
+    return Path(r);
+  } else {
+    std::string error;
+    std::wstring new_path;
+    if (!AsAbsoluteWindowsPath(
+            path_ + L"\\" + CstringToWstring(r.c_str()).get(), &new_path,
+            &error)) {
+      BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
+          << "Path::GetRelative failed: " << error;
+    }
+    return Path(new_path);
+  }
+}
+
+bool Path::IsNull() const { return path_ == L"NUL"; }
+
+std::string Path::AsPrintablePath() const {
+  return WstringToCstring(RemoveUncPrefixMaybe(path_.c_str())).get();
+}
+
 }  // namespace blaze_util
diff --git a/src/test/cpp/util/path_posix_test.cc b/src/test/cpp/util/path_posix_test.cc
index 7c08490..5bf1826 100644
--- a/src/test/cpp/util/path_posix_test.cc
+++ b/src/test/cpp/util/path_posix_test.cc
@@ -184,4 +184,54 @@
             JoinPath(GetCwd(), "%PATH%"));
 }
 
+TEST(PathPosixTest, NormalizeAbsPath) {
+  EXPECT_EQ(TestOnly_NormalizeAbsPath(""), "");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("not_absolute"), "");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("../x/y"), "");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("./"), "");
+
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/."), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/./"), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("//.//"), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/."), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("//."), "/");
+
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/../"), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("//..//"), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/.."), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("//.."), "/");
+
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/x"), "/x");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/x/"), "/x/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("//x//"), "/x/");
+
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/x/.."), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/x/../"), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("//x//..//"), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("//x//y//"), "/x/y/");
+
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/x/./y/"), "/x/y/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/x/../y/"), "/y/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/x/../../y/"), "/y/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/x/../y/.."), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/x/../y/../"), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/./x/../y/../"), "/");
+
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/foo"), "/foo");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/foo/"), "/foo/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("//foo//"), "/foo/");
+
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/foo/.."), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/foo/../"), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("//foo//..//"), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("//foo//bar//"), "/foo/bar/");
+
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/foo/./bar/"), "/foo/bar/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/foo/../bar/"), "/bar/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/foo/../../bar/"), "/bar/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/foo/../bar/.."), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/foo/../bar/../"), "/");
+  EXPECT_EQ(TestOnly_NormalizeAbsPath("/./foo/../bar/../"), "/");
+}
+
 }  // namespace blaze_util