[cpp-vfs] Implement vfs::Service.

DX-387 #comment

TEST=fx run-test vfs_cpp_tests

Change-Id: I5356bb65644defae46c282a39fa289f5c86a04ee
diff --git a/garnet/public/lib/vfs/cpp/BUILD.gn b/garnet/public/lib/vfs/cpp/BUILD.gn
index da3eb1f3..77fd660 100644
--- a/garnet/public/lib/vfs/cpp/BUILD.gn
+++ b/garnet/public/lib/vfs/cpp/BUILD.gn
@@ -30,6 +30,8 @@
     "pseudo_dir.h",
     "pseudo_file.cc",
     "pseudo_file.h",
+    "service.cc",
+    "service.h",
   ]
 
   public_deps = [
@@ -47,10 +49,12 @@
     "file_unittest.cc",
     "pseudo_dir_unittest.cc",
     "pseudo_file_unittest.cc",
+    "service_unittest.cc",
   ]
 
   deps = [
     ":cpp",
+    "//garnet/examples/fidl/services:echo2",
     "//garnet/public/lib/gtest",
     "//third_party/googletest:gtest_main",
   ]
diff --git a/garnet/public/lib/vfs/cpp/directory.cc b/garnet/public/lib/vfs/cpp/directory.cc
index d8f7b3a..7b2c597 100644
--- a/garnet/public/lib/vfs/cpp/directory.cc
+++ b/garnet/public/lib/vfs/cpp/directory.cc
@@ -28,6 +28,8 @@
 }
 
 uint32_t Directory::GetAdditionalAllowedFlags() const {
+  // TODO(ZX-3251): overide this in PseudoDir and remove OPEN_RIGHT_WRITABLE
+  // flag.
   return fuchsia::io::OPEN_RIGHT_READABLE | fuchsia::io::OPEN_RIGHT_WRITABLE |
          fuchsia::io::OPEN_FLAG_DIRECTORY;
 }
diff --git a/garnet/public/lib/vfs/cpp/node.cc b/garnet/public/lib/vfs/cpp/node.cc
index 0cfbaad..0dc3c31 100644
--- a/garnet/public/lib/vfs/cpp/node.cc
+++ b/garnet/public/lib/vfs/cpp/node.cc
@@ -110,6 +110,12 @@
     SendOnOpenEventOnError(flags, std::move(request), status);
     return status;
   }
+  return Connect(flags, std::move(request), dispatcher);
+}
+
+zx_status_t Node::Connect(uint32_t flags, zx::channel request,
+                          async_dispatcher_t* dispatcher) {
+  zx_status_t status;
   std::unique_ptr<Connection> connection;
   if (Flags::IsPathOnly(flags)) {
     status = Node::CreateConnection(flags, &connection);
diff --git a/garnet/public/lib/vfs/cpp/node.h b/garnet/public/lib/vfs/cpp/node.h
index b00169a..212c3bf 100644
--- a/garnet/public/lib/vfs/cpp/node.h
+++ b/garnet/public/lib/vfs/cpp/node.h
@@ -85,8 +85,7 @@
   // |async_get_default_dispatcher| to obtain the default dispatcher for the
   // current thread.
   //
-  // Uses |CreateConnection| to create a connection appropriate for the concrete
-  // type of this object.
+  // Calls |Connect| after validating flags and modes.
   zx_status_t Serve(uint32_t flags, zx::channel request,
                     async_dispatcher_t* dispatcher = nullptr);
 
@@ -109,6 +108,16 @@
   virtual zx_status_t Lookup(const std::string& name, Node** out_node) const;
 
  protected:
+  // Called by |Serve| after validating flags and modes.
+  // This should be implemented by sub classes which doesn't create a connection
+  // class.
+  //
+  // Default implementation:
+  // Uses |CreateConnection| to create a connection appropriate for the concrete
+  // type of this object.
+  virtual zx_status_t Connect(uint32_t flags, zx::channel request,
+                              async_dispatcher_t* dispatcher);
+
   const std::vector<std::unique_ptr<Connection>>& connections() const {
     return connections_;
   }
diff --git a/garnet/public/lib/vfs/cpp/service.cc b/garnet/public/lib/vfs/cpp/service.cc
new file mode 100644
index 0000000..ba723f7f
--- /dev/null
+++ b/garnet/public/lib/vfs/cpp/service.cc
@@ -0,0 +1,57 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <lib/vfs/cpp/service.h>
+
+#include <lib/fdio/vfs.h>
+#include <lib/vfs/cpp/flags.h>
+
+namespace vfs {
+
+Service::Service(Connector connector) : connector_(std::move(connector)) {}
+
+Service::~Service() = default;
+
+void Service::Describe(fuchsia::io::NodeInfo* out_info) {
+  out_info->set_service(fuchsia::io::Service());
+}
+
+zx_status_t Service::CreateConnection(uint32_t flags,
+                                      std::unique_ptr<Connection>* connection) {
+  return ZX_ERR_NOT_SUPPORTED;
+}
+
+uint32_t Service::GetAdditionalAllowedFlags() const {
+  // TODO(ZX-3251): remove OPEN_RIGHT_WRITABLE flag
+  return fuchsia::io::OPEN_RIGHT_READABLE | fuchsia::io::OPEN_RIGHT_WRITABLE;
+}
+
+zx_status_t Service::Connect(uint32_t flags, zx::channel request,
+                             async_dispatcher_t* dispatcher) {
+  if (Flags::IsPathOnly(flags)) {
+    return Node::Connect(flags, std::move(request), dispatcher);
+  }
+  if (connector_ == nullptr) {
+    SendOnOpenEventOnError(flags, std::move(request), ZX_ERR_NOT_SUPPORTED);
+    return ZX_ERR_NOT_SUPPORTED;
+  }
+  connector_(std::move(request), dispatcher);
+  return ZX_OK;
+}
+
+bool Service::IsDirectory() const { return false; }
+
+zx_status_t Service::GetAttr(
+    fuchsia::io::NodeAttributes* out_attributes) const {
+  out_attributes->mode = fuchsia::io::MODE_TYPE_SERVICE | V_IRUSR;
+  out_attributes->id = fuchsia::io::INO_UNKNOWN;
+  out_attributes->content_size = 0;
+  out_attributes->storage_size = 0;
+  out_attributes->link_count = 1;
+  out_attributes->creation_time = 0;
+  out_attributes->modification_time = 0;
+  return ZX_OK;
+}
+
+}  // namespace vfs
\ No newline at end of file
diff --git a/garnet/public/lib/vfs/cpp/service.h b/garnet/public/lib/vfs/cpp/service.h
new file mode 100644
index 0000000..8fcf1a2
--- /dev/null
+++ b/garnet/public/lib/vfs/cpp/service.h
@@ -0,0 +1,76 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_VFS_CPP_SERVICE_H_
+#define LIB_VFS_CPP_SERVICE_H_
+
+#include <fuchsia/io/cpp/fidl.h>
+#include <lib/vfs/cpp/node.h>
+
+namespace vfs {
+
+// A |Node| which binds a channel to a service implementation when opened.
+//
+// This class is thread-safe.
+class Service : public Node {
+ public:
+  // Handler called to bind the provided channel to an implementation
+  // of the service.
+  using Connector =
+      fit::function<void(zx::channel channel, async_dispatcher_t* dispatcher)>;
+
+  // Adds the specified interface to the set of public interfaces.
+  //
+  // Creates |Service| with a |connector| with the given |service_name|, using
+  // the given |interface_request_handler|. |interface_request_handler| should
+  // remain valid for the lifetime of this object.
+  //
+  // A typical usage may be:
+  //
+  //   vfs::Service foo_service(foobar_bindings_.GetHandler(this, dispatcher));
+  //
+  // For now this implementation ignores |dispatcher| that we get from |Serve|
+  // call, if you want to use dispatcher call |Service(Connector)|.
+  template <typename Interface>
+  explicit Service(fidl::InterfaceRequestHandler<Interface> handler)
+      : Service([handler = std::move(handler)](zx::channel channel,
+                                               async_dispatcher_t* dispatcher) {
+          handler(fidl::InterfaceRequest<Interface>(std::move(channel)));
+        }) {}
+
+  // Creates a service with the specified connector.
+  //
+  // If the |connector| is null, then incoming connection requests will be
+  // dropped.
+  explicit Service(Connector connector);
+
+  // Destroys the services and releases its connector.
+  ~Service() override;
+
+  // |Node| implementation:
+  zx_status_t GetAttr(fuchsia::io::NodeAttributes* out_attributes) const final;
+
+  void Describe(fuchsia::io::NodeInfo* out_info) override final;
+
+  const Connector& connector() const { return connector_; }
+
+ protected:
+  // |Node| implementations:
+  zx_status_t CreateConnection(uint32_t flags,
+                               std::unique_ptr<Connection>* connection) final;
+
+  uint32_t GetAdditionalAllowedFlags() const override final;
+
+  bool IsDirectory() const override final;
+
+  zx_status_t Connect(uint32_t flags, zx::channel request,
+                      async_dispatcher_t* dispatcher) override;
+
+ private:
+  Connector connector_;
+};
+
+}  // namespace vfs
+
+#endif  // LIB_VFS_CPP_SERVICE_H_
\ No newline at end of file
diff --git a/garnet/public/lib/vfs/cpp/service_unittest.cc b/garnet/public/lib/vfs/cpp/service_unittest.cc
new file mode 100644
index 0000000..da1bef1
--- /dev/null
+++ b/garnet/public/lib/vfs/cpp/service_unittest.cc
@@ -0,0 +1,136 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "lib/vfs/cpp/service.h"
+
+#include <fidl/examples/echo/cpp/fidl.h>
+#include <lib/fdio/vfs.h>
+#include <lib/fidl/cpp/binding_set.h>
+#include "lib/gtest/real_loop_fixture.h"
+#include "lib/vfs/cpp/pseudo_dir.h"
+
+class ServiceTest : public gtest::RealLoopFixture,
+                    public fidl::examples::echo::Echo {
+  void EchoString(fidl::StringPtr value, EchoStringCallback callback) override {
+    callback(answer_);
+  }
+
+ protected:
+  ServiceTest()
+      : answer_("my_fake_ans"),
+        service_name_("echo_service"),
+        second_loop_(&kAsyncLoopConfigNoAttachToThread) {
+    auto service = std::make_unique<vfs::Service>(
+        bindings_.GetHandler(this, second_loop_.dispatcher()));
+
+    dir_.Serve(0, dir_ptr_.NewRequest().TakeChannel(),
+               second_loop_.dispatcher());
+    dir_.AddEntry(service_name_, std::move(service));
+    second_loop_.StartThread("vfs test thread");
+  }
+
+  const std::string& answer() const { return answer_; }
+
+  const std::string& service_name() const { return service_name_; }
+
+  void AssertInValidOpen(uint32_t flag, uint32_t mode,
+                         zx_status_t expected_status) {
+    SCOPED_TRACE("flag: " + std::to_string(flag) +
+                 ", mode: " + std::to_string(mode));
+    fuchsia::io::NodePtr node_ptr;
+    dir_ptr()->Open(flag | fuchsia::io::OPEN_FLAG_DESCRIBE, mode,
+                    service_name(), node_ptr.NewRequest());
+
+    bool on_open_called = false;
+
+    node_ptr.events().OnOpen =
+        [&](zx_status_t status, std::unique_ptr<fuchsia::io::NodeInfo> unused) {
+          EXPECT_FALSE(on_open_called);  // should be called only once
+          on_open_called = true;
+          EXPECT_EQ(expected_status, status);
+        };
+
+    ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&]() { return on_open_called; },
+                                          zx::sec(1), zx::msec(1)));
+  }
+
+  fuchsia::io::DirectoryPtr& dir_ptr() { return dir_ptr_; }
+
+ private:
+  std::string answer_;
+  std::string service_name_;
+  fidl::BindingSet<Echo> bindings_;
+  vfs::PseudoDir dir_;
+  fuchsia::io::DirectoryPtr dir_ptr_;
+  async::Loop second_loop_;
+};
+
+TEST_F(ServiceTest, CanOpenAsNodeReferenceAndTestGetAttr) {
+  fuchsia::io::NodeSyncPtr ptr;
+  dir_ptr()->Open(fuchsia::io::OPEN_FLAG_NODE_REFERENCE, 0, service_name(),
+                  ptr.NewRequest());
+
+  zx_status_t s;
+  fuchsia::io::NodeAttributes attr;
+  ptr->GetAttr(&s, &attr);
+  EXPECT_EQ(ZX_OK, s);
+  EXPECT_EQ(fuchsia::io::MODE_TYPE_SERVICE,
+            attr.mode & fuchsia::io::MODE_TYPE_SERVICE);
+}
+
+TEST_F(ServiceTest, TestDescribe) {
+  fuchsia::io::NodeSyncPtr ptr;
+  dir_ptr()->Open(fuchsia::io::OPEN_FLAG_NODE_REFERENCE, 0, service_name(),
+                  ptr.NewRequest());
+
+  fuchsia::io::NodeInfo info;
+  ptr->Describe(&info);
+  EXPECT_TRUE(info.is_service());
+}
+
+TEST_F(ServiceTest, CanOpenAsAService) {
+  uint32_t flags[] = {0, fuchsia::io::OPEN_RIGHT_READABLE,
+                      fuchsia::io::OPEN_RIGHT_WRITABLE};
+  uint32_t modes[] = {
+      0, fuchsia::io::MODE_TYPE_SERVICE, V_IRWXU, V_IRUSR, V_IWUSR, V_IXUSR};
+
+  for (uint32_t mode : modes) {
+    for (uint32_t flag : flags) {
+      SCOPED_TRACE("flag: " + std::to_string(flag) +
+                   ", mode: " + std::to_string(mode));
+      fidl::examples::echo::EchoSyncPtr ptr;
+      dir_ptr()->Open(flag, mode, service_name(),
+                      fidl::InterfaceRequest<fuchsia::io::Node>(
+                          ptr.NewRequest().TakeChannel()));
+
+      fidl::StringPtr ans;
+      ptr->EchoString("hello", &ans);
+      EXPECT_EQ(answer(), ans);
+    }
+  }
+}
+
+TEST_F(ServiceTest, CannotOpenServiceWithInvalidFlags) {
+  uint32_t flags[] = {fuchsia::io::OPEN_RIGHT_ADMIN,
+                      fuchsia::io::OPEN_FLAG_CREATE,
+                      fuchsia::io::OPEN_FLAG_CREATE_IF_ABSENT,
+                      fuchsia::io::OPEN_FLAG_TRUNCATE,
+                      fuchsia::io::OPEN_FLAG_APPEND,
+                      fuchsia::io::OPEN_FLAG_NO_REMOTE};
+
+  for (uint32_t flag : flags) {
+    AssertInValidOpen(flag, 0, ZX_ERR_NOT_SUPPORTED);
+  }
+  AssertInValidOpen(fuchsia::io::OPEN_FLAG_DIRECTORY, 0, ZX_ERR_NOT_DIR);
+}
+
+TEST_F(ServiceTest, CannotOpenServiceWithInvalidMode) {
+  uint32_t modes[] = {
+      fuchsia::io::MODE_TYPE_DIRECTORY, fuchsia::io::MODE_TYPE_BLOCK_DEVICE,
+      fuchsia::io::MODE_TYPE_FILE, fuchsia::io::MODE_TYPE_SOCKET};
+
+  for (uint32_t mode : modes) {
+    AssertInValidOpen(0, mode, ZX_ERR_INVALID_ARGS);
+  }
+}
\ No newline at end of file