util: FaceUri: add syntax for NIC-associated permanent faces

refs #3522

Change-Id: Id2c9f751d7d1f65b2f0ef968aad660e95250d155
diff --git a/src/util/face-uri.cpp b/src/util/face-uri.cpp
index 4ad9dba..acdbc11 100644
--- a/src/util/face-uri.cpp
+++ b/src/util/face-uri.cpp
@@ -68,14 +68,14 @@
   m_port.clear();
   m_path.clear();
 
-  static const boost::regex protocolExp("(\\w+\\d?)://([^/]*)(\\/[^?]*)?");
+  static const boost::regex protocolExp("(\\w+\\d?(\\+\\w+)?)://([^/]*)(\\/[^?]*)?");
   boost::smatch protocolMatch;
   if (!boost::regex_match(uri, protocolMatch, protocolExp)) {
     return false;
   }
   m_scheme = protocolMatch[1];
-  const std::string& authority = protocolMatch[2];
-  m_path = protocolMatch[3];
+  const std::string& authority = protocolMatch[3];
+  m_path = protocolMatch[4];
 
   // pattern for IPv6 address enclosed in [ ], with optional port number
   static const boost::regex v6Exp("^\\[([a-fA-F0-9:]+)\\](?:\\:(\\d+))?$");
@@ -112,7 +112,7 @@
   m_isV6 = endpoint.address().is_v6();
   m_scheme = m_isV6 ? "udp6" : "udp4";
   m_host = endpoint.address().to_string();
-  m_port = boost::lexical_cast<std::string>(endpoint.port());
+  m_port = to_string(endpoint.port());
 }
 
 FaceUri::FaceUri(const boost::asio::ip::tcp::endpoint& endpoint)
@@ -120,7 +120,7 @@
   m_isV6 = endpoint.address().is_v6();
   m_scheme = m_isV6 ? "tcp6" : "tcp4";
   m_host = endpoint.address().to_string();
-  m_port = boost::lexical_cast<std::string>(endpoint.port());
+  m_port = to_string(endpoint.port());
 }
 
 FaceUri::FaceUri(const boost::asio::ip::tcp::endpoint& endpoint, const std::string& scheme)
@@ -128,7 +128,7 @@
 {
   m_isV6 = endpoint.address().is_v6();
   m_host = endpoint.address().to_string();
-  m_port = boost::lexical_cast<std::string>(endpoint.port());
+  m_port = to_string(endpoint.port());
 }
 
 #ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
@@ -145,7 +145,7 @@
 {
   FaceUri uri;
   uri.m_scheme = "fd";
-  uri.m_host = boost::lexical_cast<std::string>(fd);
+  uri.m_host = to_string(fd);
   return uri;
 }
 
@@ -165,6 +165,16 @@
   return uri;
 }
 
+FaceUri
+FaceUri::fromUdpDev(const boost::asio::ip::udp::endpoint& endpoint, const std::string& ifname)
+{
+  FaceUri uri;
+  uri.m_scheme = endpoint.address().is_v6() ? "udp6+dev" : "udp4+dev";
+  uri.m_host = ifname;
+  uri.m_port = to_string(endpoint.port());
+  return uri;
+}
+
 bool
 FaceUri::operator==(const FaceUri& rhs) const
 {
@@ -445,10 +455,47 @@
   }
 };
 
+class UdpDevCanonizeProvider : public CanonizeProvider
+{
+public:
+  virtual std::set<std::string>
+  getSchemes() const override
+  {
+    return {"udp4+dev", "udp6+dev"};
+  }
+
+  virtual bool
+  isCanonical(const FaceUri& faceUri) const override
+  {
+    if (faceUri.getPort().empty()) {
+      return false;
+    }
+    if (!faceUri.getPath().empty()) {
+      return false;
+    }
+    return true;
+  }
+
+  virtual void
+  canonize(const FaceUri& faceUri,
+           const FaceUri::CanonizeSuccessCallback& onSuccess,
+           const FaceUri::CanonizeFailureCallback& onFailure,
+           boost::asio::io_service& io, const time::nanoseconds& timeout) const override
+  {
+    if (this->isCanonical(faceUri)) {
+      onSuccess(faceUri);
+    }
+    else {
+      onFailure("cannot canonize " + faceUri.toString());
+    }
+  }
+};
+
 typedef boost::mpl::vector<
     UdpCanonizeProvider*,
     TcpCanonizeProvider*,
-    EtherCanonizeProvider*
+    EtherCanonizeProvider*,
+    UdpDevCanonizeProvider*
   > CanonizeProviders;
 typedef std::map<std::string, shared_ptr<CanonizeProvider> > CanonizeProviderTable;
 
diff --git a/src/util/face-uri.hpp b/src/util/face-uri.hpp
index f7cbe41..b222323 100644
--- a/src/util/face-uri.hpp
+++ b/src/util/face-uri.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014,  Regents of the University of California,
+ * Copyright (c) 2014-2016,  Regents of the University of California,
  *                      Arizona Board of Regents,
  *                      Colorado State University,
  *                      University Pierre & Marie Curie, Sorbonne University,
@@ -103,6 +103,10 @@
   static FaceUri
   fromDev(const std::string& ifname);
 
+  /// create udp4 or udp6 NIC-associated FaceUri from endpoint and network device name
+  static FaceUri
+  fromUdpDev(const boost::asio::ip::udp::endpoint& endpoint, const std::string& ifname);
+
 public: // getters
   /// get scheme (protocol)
   const std::string&
diff --git a/tests/unit-tests/util/face-uri.t.cpp b/tests/unit-tests/util/face-uri.t.cpp
index c3246a7..b860e2f 100644
--- a/tests/unit-tests/util/face-uri.t.cpp
+++ b/tests/unit-tests/util/face-uri.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2013-2015 Regents of the University of California,
+ * Copyright (c) 2013-2016 Regents of the University of California,
  *                         Arizona Board of Regents,
  *                         Colorado State University,
  *                         University Pierre & Marie Curie, Sorbonne University,
@@ -417,6 +417,50 @@
   BOOST_CHECK_EQUAL(FaceUri::fromDev(ifname).toString(), "dev://en1");
 }
 
+BOOST_AUTO_TEST_CASE(ParseUdpDev)
+{
+  FaceUri uri;
+
+  BOOST_CHECK(uri.parse("udp4+dev://eth0:7777"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "udp4+dev");
+  BOOST_CHECK_EQUAL(uri.getHost(), "eth0");
+  BOOST_CHECK_EQUAL(uri.getPort(), "7777");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK(uri.parse("udp6+dev://eth1:7777"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "udp6+dev");
+  BOOST_CHECK_EQUAL(uri.getHost(), "eth1");
+  BOOST_CHECK_EQUAL(uri.getPort(), "7777");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK(uri.parse("abc+efg://eth0"));
+  BOOST_CHECK(!uri.parse("abc+://eth0"));
+  BOOST_CHECK(!uri.parse("+abc://eth0"));
+
+  using namespace boost::asio;
+
+  ip::udp::endpoint endpoint4(ip::udp::v4(), 7777);
+  BOOST_REQUIRE_NO_THROW(FaceUri::fromUdpDev(endpoint4, "en1"));
+  BOOST_CHECK_EQUAL(FaceUri::fromUdpDev(endpoint4, "en1").toString(), "udp4+dev://en1:7777");
+
+  ip::udp::endpoint endpoint6(ip::udp::v6(), 7777);
+  BOOST_REQUIRE_NO_THROW(FaceUri::fromUdpDev(endpoint6, "en2"));
+  BOOST_CHECK_EQUAL(FaceUri::fromUdpDev(endpoint6, "en2").toString(), "udp6+dev://en2:7777");
+}
+
+BOOST_FIXTURE_TEST_CASE(CanonizeUdpDev, CanonizeFixture)
+{
+  BOOST_CHECK_EQUAL(FaceUri("udp4+dev://eth0:7777").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("udp6+dev://eth1:7777").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("udp+dev://eth1:7777").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp6+dev://eth1").isCanonical(), false);
+
+  addTest("udp4+dev://en0:7777", true, "udp4+dev://en0:7777");
+  addTest("udp6+dev://en0:7777", true, "udp6+dev://en0:7777");
+  addTest("udp+dev://en1:7777", false, "");
+  addTest("udp6+dev://en2", false, "");
+}
+
 BOOST_AUTO_TEST_CASE(CanonizeEmptyCallback)
 {
   boost::asio::io_service io;