[fxl][files] add JoinPath

TESTED=`fx run-host-tests fxl_unittests -- --gtest_filter=Path.JoinPath`

Change-Id: I2da21e27e458a61fa678c217e44ada592654b134
diff --git a/public/lib/fxl/files/path.cc b/public/lib/fxl/files/path.cc
index 6912481..a217da5 100644
--- a/public/lib/fxl/files/path.cc
+++ b/public/lib/fxl/files/path.cc
@@ -238,4 +238,23 @@
   return true;
 }
 
+std::string JoinPath(const std::string& path1, const std::string& path2) {
+  if (path1.empty()) {
+    return path2;
+  }
+  if (path2.empty()) {
+    return path1;
+  }
+  if (path1.back() == '/') {
+    if (path2.front() == '/') {
+      return path1 + path2.substr(1);
+    }
+  } else {
+    if (path2.front() != '/') {
+      return path1 + "/" + path2;
+    }
+  }
+  return path1 + path2;
+}
+
 }  // namespace files
diff --git a/public/lib/fxl/files/path.h b/public/lib/fxl/files/path.h
index c4a0c89..38743bb 100644
--- a/public/lib/fxl/files/path.h
+++ b/public/lib/fxl/files/path.h
@@ -36,6 +36,26 @@
 FXL_EXPORT bool DeletePathAt(int root_fd, const std::string& path,
                              bool recursive);
 
+// Joins two paths together.
+// Regardless if |path1| has a trailing '/' or |path2| has a leading '/', there
+// will be only one '/' in-between in the joined path.
+// Note that if either path is "" then the other path is returned unchanged.
+//
+// JoinPath("/foo",   "bar") -> "/foo/bar"
+// JoinPath("/foo",  "/bar") -> "/foo/bar"
+// JoinPath("/foo/",  "bar") -> "/foo/bar"
+// JoinPath("/foo/", "/bar") -> "/foo/bar"
+//
+// JoinPath("",      "") -> ""
+// JoinPath("",  "/foo") -> "/foo"
+// JoinPath("",   "foo") -> "foo"
+// JoinPath("/foo",  "") -> "/foo"
+// JoinPath("foo",   "") -> "foo"
+// JoinPath("/foo/", "") -> "/foo/"
+// JoinPath("foo/",  "") -> "foo/"
+FXL_EXPORT std::string JoinPath(const std::string& path1,
+                                const std::string& path2);
+
 }  // namespace files
 
 #endif  // LIB_FXL_FILES_PATH_H_
diff --git a/public/lib/fxl/files/path_unittest.cc b/public/lib/fxl/files/path_unittest.cc
index b0b5c45..ebff43f 100644
--- a/public/lib/fxl/files/path_unittest.cc
+++ b/public/lib/fxl/files/path_unittest.cc
@@ -241,5 +241,41 @@
   EXPECT_FALSE(IsDirectoryAt(root.get(), sub_sub_dir1));
 }
 
+TEST(Path, JoinPath) {
+  EXPECT_EQ(JoinPath("foo", ""), "foo");
+  EXPECT_EQ(JoinPath("foo", "bar"), "foo/bar");
+  EXPECT_EQ(JoinPath("foo", "bar/"), "foo/bar/");
+  EXPECT_EQ(JoinPath("foo", "/bar"), "foo/bar");
+  EXPECT_EQ(JoinPath("foo", "/bar/"), "foo/bar/");
+
+  EXPECT_EQ(JoinPath("foo/", ""), "foo/");
+  EXPECT_EQ(JoinPath("foo/", ""), "foo/");
+  EXPECT_EQ(JoinPath("foo/", "bar"), "foo/bar");
+  EXPECT_EQ(JoinPath("foo/", "bar/"), "foo/bar/");
+  EXPECT_EQ(JoinPath("foo/", "/bar"), "foo/bar");
+  EXPECT_EQ(JoinPath("foo/", "/bar/"), "foo/bar/");
+
+  EXPECT_EQ(JoinPath("/foo", ""), "/foo");
+  EXPECT_EQ(JoinPath("/foo", "bar"), "/foo/bar");
+  EXPECT_EQ(JoinPath("/foo", "bar/"), "/foo/bar/");
+  EXPECT_EQ(JoinPath("/foo", "/bar"), "/foo/bar");
+  EXPECT_EQ(JoinPath("/foo", "/bar/"), "/foo/bar/");
+
+  EXPECT_EQ(JoinPath("/foo/", ""), "/foo/");
+  EXPECT_EQ(JoinPath("/foo/", "bar"), "/foo/bar");
+  EXPECT_EQ(JoinPath("/foo/", "bar/"), "/foo/bar/");
+  EXPECT_EQ(JoinPath("/foo/", "/bar"), "/foo/bar");
+  EXPECT_EQ(JoinPath("/foo/", "/bar/"), "/foo/bar/");
+
+  EXPECT_EQ(JoinPath("", ""), "");
+  EXPECT_EQ(JoinPath("", "bar"), "bar");
+  EXPECT_EQ(JoinPath("", "bar/"), "bar/");
+  EXPECT_EQ(JoinPath("", "/bar"), "/bar");
+  EXPECT_EQ(JoinPath("", "/bar/"), "/bar/");
+
+  EXPECT_EQ(JoinPath("/foo/bar/baz/", "/blah/blink/biz"),
+            "/foo/bar/baz/blah/blink/biz");
+}
+
 }  // namespace
 }  // namespace files