mgmt: declare cs/erase command

refs #4318

Change-Id: If34ba8d55a4d46d53f552f4edd748623d4c0e55e
diff --git a/src/mgmt/nfd/control-command.cpp b/src/mgmt/nfd/control-command.cpp
index 9f978f0..556a0ed 100644
--- a/src/mgmt/nfd/control-command.cpp
+++ b/src/mgmt/nfd/control-command.cpp
@@ -270,6 +270,38 @@
     .required(CONTROL_PARAMETER_FLAGS);
 }
 
+CsEraseCommand::CsEraseCommand()
+  : ControlCommand("cs", "erase")
+{
+  m_requestValidator
+    .required(CONTROL_PARAMETER_NAME)
+    .optional(CONTROL_PARAMETER_N_CS_ENTRIES);
+  m_responseValidator
+    .required(CONTROL_PARAMETER_NAME)
+    .optional(CONTROL_PARAMETER_CAPACITY)
+    .required(CONTROL_PARAMETER_N_CS_ENTRIES);
+}
+
+void
+CsEraseCommand::validateRequest(const ControlParameters& parameters) const
+{
+  this->ControlCommand::validateRequest(parameters);
+
+  if (parameters.hasNCsEntries() && parameters.getNCsEntries() == 0) {
+    BOOST_THROW_EXCEPTION(ArgumentError("NCsEntries must be positive"));
+  }
+}
+
+void
+CsEraseCommand::validateResponse(const ControlParameters& parameters) const
+{
+  this->ControlCommand::validateResponse(parameters);
+
+  if (parameters.hasCapacity() && parameters.getCapacity() == 0) {
+    BOOST_THROW_EXCEPTION(ArgumentError("Capacity must be positive"));
+  }
+}
+
 StrategyChoiceSetCommand::StrategyChoiceSetCommand()
   : ControlCommand("strategy-choice", "set")
 {
diff --git a/src/mgmt/nfd/control-command.hpp b/src/mgmt/nfd/control-command.hpp
index 74f0bb1..90d7d38 100644
--- a/src/mgmt/nfd/control-command.hpp
+++ b/src/mgmt/nfd/control-command.hpp
@@ -155,7 +155,7 @@
 /**
  * \ingroup management
  * \brief represents a faces/update command
- * \sa https://redmine.named-data.net/projects/nfd/wiki/FaceMgmt#Update-a-face
+ * \sa https://redmine.named-data.net/projects/nfd/wiki/FaceMgmt#Update-the-static-properties-of-a-face
  */
 class FaceUpdateCommand : public ControlCommand
 {
@@ -231,7 +231,7 @@
 /**
  * \ingroup management
  * \brief represents a cs/config command
- * \sa https://redmine.named-data.net/projects/nfd/wiki/CsMgmt#Update-config
+ * \sa https://redmine.named-data.net/projects/nfd/wiki/CsMgmt#Update-configuration
  */
 class CsConfigCommand : public ControlCommand
 {
@@ -242,6 +242,24 @@
 
 /**
  * \ingroup management
+ * \brief represents a cs/erase command
+ * \sa https://redmine.named-data.net/projects/nfd/wiki/CsMgmt#Erase-entries
+ */
+class CsEraseCommand : public ControlCommand
+{
+public:
+  CsEraseCommand();
+
+  void
+  validateRequest(const ControlParameters& parameters) const override;
+
+  void
+  validateResponse(const ControlParameters& parameters) const override;
+};
+
+
+/**
+ * \ingroup management
  * \brief represents a strategy-choice/set command
  * \sa https://redmine.named-data.net/projects/nfd/wiki/StrategyChoice#Set-the-strategy-for-a-namespace
  */
diff --git a/src/mgmt/nfd/control-parameters.cpp b/src/mgmt/nfd/control-parameters.cpp
index 1e4d9fe..c0377c5 100644
--- a/src/mgmt/nfd/control-parameters.cpp
+++ b/src/mgmt/nfd/control-parameters.cpp
@@ -76,6 +76,9 @@
   if (this->hasFlags()) {
     totalLength += prependNonNegativeIntegerBlock(encoder, tlv::nfd::Flags, m_flags);
   }
+  if (this->hasNCsEntries()) {
+    totalLength += prependNonNegativeIntegerBlock(encoder, tlv::nfd::NCsEntries, m_nCsEntries);
+  }
   if (this->hasCapacity()) {
     totalLength += prependNonNegativeIntegerBlock(encoder, tlv::nfd::Capacity, m_capacity);
   }
@@ -173,6 +176,12 @@
     m_capacity = readNonNegativeInteger(*val);
   }
 
+  val = m_wire.find(tlv::nfd::NCsEntries);
+  m_hasFields[CONTROL_PARAMETER_N_CS_ENTRIES] = val != m_wire.elements_end();
+  if (this->hasNCsEntries()) {
+    m_nCsEntries = readNonNegativeInteger(*val);
+  }
+
   val = m_wire.find(tlv::nfd::Flags);
   m_hasFields[CONTROL_PARAMETER_FLAGS] = val != m_wire.elements_end();
   if (this->hasFlags()) {
@@ -328,6 +337,10 @@
     os << "Capacity: " << parameters.getCapacity() << ", ";
   }
 
+  if (parameters.hasNCsEntries()) {
+    os << "NCsEntries: " << parameters.getNCsEntries() << ", ";
+  }
+
   if (parameters.hasFlags()) {
     os << "Flags: " << AsHex{parameters.getFlags()} << ", ";
   }
diff --git a/src/mgmt/nfd/control-parameters.hpp b/src/mgmt/nfd/control-parameters.hpp
index ca1d75d..7005e9d 100644
--- a/src/mgmt/nfd/control-parameters.hpp
+++ b/src/mgmt/nfd/control-parameters.hpp
@@ -41,6 +41,7 @@
   CONTROL_PARAMETER_ORIGIN,
   CONTROL_PARAMETER_COST,
   CONTROL_PARAMETER_CAPACITY,
+  CONTROL_PARAMETER_N_CS_ENTRIES,
   CONTROL_PARAMETER_FLAGS,
   CONTROL_PARAMETER_MASK,
   CONTROL_PARAMETER_STRATEGY,
@@ -59,6 +60,7 @@
   "Origin",
   "Cost",
   "Capacity",
+  "NCsEntries",
   "Flags",
   "Mask",
   "Strategy",
@@ -314,6 +316,36 @@
   }
 
   bool
+  hasNCsEntries() const
+  {
+    return m_hasFields[CONTROL_PARAMETER_N_CS_ENTRIES];
+  }
+
+  uint64_t
+  getNCsEntries() const
+  {
+    BOOST_ASSERT(this->hasNCsEntries());
+    return m_nCsEntries;
+  }
+
+  ControlParameters&
+  setNCsEntries(uint64_t nCsEntries)
+  {
+    m_wire.reset();
+    m_nCsEntries = nCsEntries;
+    m_hasFields[CONTROL_PARAMETER_N_CS_ENTRIES] = true;
+    return *this;
+  }
+
+  ControlParameters&
+  unsetNCsEntries()
+  {
+    m_wire.reset();
+    m_hasFields[CONTROL_PARAMETER_N_CS_ENTRIES] = false;
+    return *this;
+  }
+
+  bool
   hasFlags() const
   {
     return m_hasFields[CONTROL_PARAMETER_FLAGS];
@@ -575,6 +607,7 @@
   RouteOrigin         m_origin;
   uint64_t            m_cost;
   uint64_t            m_capacity;
+  uint64_t            m_nCsEntries;
   uint64_t            m_flags;
   uint64_t            m_mask;
   Name                m_strategy;
diff --git a/tests/unit-tests/mgmt/nfd/control-command.t.cpp b/tests/unit-tests/mgmt/nfd/control-command.t.cpp
index 0cc3642..d76b67c 100644
--- a/tests/unit-tests/mgmt/nfd/control-command.t.cpp
+++ b/tests/unit-tests/mgmt/nfd/control-command.t.cpp
@@ -299,6 +299,69 @@
   command.validateResponse(p3);
 }
 
+BOOST_AUTO_TEST_CASE(CsEraseRequest)
+{
+  CsEraseCommand command;
+
+  // good no-limit request
+  ControlParameters p1;
+  p1.setName("/u4LYPNU8Q");
+  command.validateRequest(p1);
+  BOOST_CHECK(Name("/PREFIX/cs/erase").isPrefixOf(command.getRequestName("/PREFIX", p1)));
+
+  // good limit-entries request
+  ControlParameters p2;
+  p2.setName("/IMw1RaLF");
+  p2.setNCsEntries(177);
+  command.validateRequest(p2);
+
+  // bad request: zero entry
+  ControlParameters p3;
+  p3.setName("/ahMID1jcib");
+  p3.setNCsEntries(0);
+  BOOST_CHECK_THROW(command.validateRequest(p3), ControlCommand::ArgumentError);
+
+  // bad request: forbidden field
+  ControlParameters p4(p2);
+  p4.setCapacity(278);
+  BOOST_CHECK_THROW(command.validateRequest(p3), ControlCommand::ArgumentError);
+}
+
+BOOST_AUTO_TEST_CASE(CsEraseResponse)
+{
+  CsEraseCommand command;
+
+  // good normal response
+  ControlParameters p1;
+  p1.setName("/TwiIwCdR");
+  p1.setNCsEntries(1);
+  command.validateResponse(p1);
+
+  // good limit exceeded request
+  ControlParameters p2;
+  p2.setName("/NMsiy44pr");
+  p2.setCapacity(360);
+  p2.setNCsEntries(360);
+  command.validateResponse(p2);
+
+  // good zero-entry response
+  ControlParameters p3;
+  p3.setName("/5f1LRPh1L");
+  p3.setNCsEntries(0);
+  command.validateResponse(p3);
+
+  // bad request: missing NCsEntries
+  ControlParameters p4(p1);
+  p4.unsetNCsEntries();
+  BOOST_CHECK_THROW(command.validateResponse(p4), ControlCommand::ArgumentError);
+
+  // bad request: zero capacity
+  ControlParameters p5(p1);
+  p5.setCapacity(0);
+  BOOST_CHECK_THROW(command.validateResponse(p5), ControlCommand::ArgumentError);
+
+}
+
 BOOST_AUTO_TEST_CASE(StrategyChoiceSet)
 {
   StrategyChoiceSetCommand command;
diff --git a/tests/unit-tests/mgmt/nfd/control-parameters.t.cpp b/tests/unit-tests/mgmt/nfd/control-parameters.t.cpp
index 80e8134..14be320 100644
--- a/tests/unit-tests/mgmt/nfd/control-parameters.t.cpp
+++ b/tests/unit-tests/mgmt/nfd/control-parameters.t.cpp
@@ -43,6 +43,7 @@
   BOOST_CHECK_EQUAL(decoded.hasOrigin(), false);
   BOOST_CHECK_EQUAL(decoded.hasCost(), false);
   BOOST_CHECK_EQUAL(decoded.hasCapacity(), false);
+  BOOST_CHECK_EQUAL(decoded.hasNCsEntries(), false);
   BOOST_CHECK_EQUAL(decoded.hasFlags(), false);
   BOOST_CHECK_EQUAL(decoded.hasMask(), false);
   BOOST_CHECK_EQUAL(decoded.hasStrategy(), false);
@@ -56,6 +57,7 @@
   input.setOrigin(ROUTE_ORIGIN_NLSR);
   input.setCost(1388);
   input.setCapacity(2632);
+  input.setNCsEntries(3100);
   input.setFlags(0xAFC4);
   input.setMask(0xF7A1);
   input.setStrategy("/strategy-name");
@@ -70,6 +72,7 @@
   BOOST_CHECK_EQUAL(decoded.hasOrigin(), true);
   BOOST_CHECK_EQUAL(decoded.hasCost(), true);
   BOOST_CHECK_EQUAL(decoded.hasCapacity(), true);
+  BOOST_CHECK_EQUAL(decoded.hasNCsEntries(), true);
   BOOST_CHECK_EQUAL(decoded.hasFlags(), true);
   BOOST_CHECK_EQUAL(decoded.hasMask(), true);
   BOOST_CHECK_EQUAL(decoded.hasStrategy(), true);
@@ -83,6 +86,7 @@
   BOOST_CHECK_EQUAL(decoded.getOrigin(), ROUTE_ORIGIN_NLSR);
   BOOST_CHECK_EQUAL(decoded.getCost(), 1388);
   BOOST_CHECK_EQUAL(decoded.getCapacity(), 2632);
+  BOOST_CHECK_EQUAL(decoded.getNCsEntries(), 3100);
   BOOST_CHECK_EQUAL(decoded.getFlags(), 0xAFC4);
   BOOST_CHECK_EQUAL(decoded.getMask(), 0xF7A1);
   BOOST_CHECK_EQUAL(decoded.getStrategy(), "/strategy-name");
@@ -96,6 +100,7 @@
   input.unsetOrigin();
   input.unsetCost();
   input.unsetCapacity();
+  input.unsetNCsEntries();
   input.unsetFlags();
   input.unsetMask();
   input.unsetStrategy();
@@ -108,6 +113,7 @@
   BOOST_CHECK_EQUAL(input.hasOrigin(), false);
   BOOST_CHECK_EQUAL(input.hasCost(), false);
   BOOST_CHECK_EQUAL(input.hasCapacity(), false);
+  BOOST_CHECK_EQUAL(input.hasNCsEntries(), false);
   BOOST_CHECK_EQUAL(input.hasFlags(), false);
   BOOST_CHECK_EQUAL(input.hasMask(), false);
   BOOST_CHECK_EQUAL(input.hasStrategy(), false);