util: add io::loadBuffer and io::saveBuffer

Change-Id: Ifa9d3bc44bba5dd8e2bd5d419629763936f355d4
diff --git a/ndn-cxx/util/io.cpp b/ndn-cxx/util/io.cpp
index ac9f2b7..a385eea 100644
--- a/ndn-cxx/util/io.cpp
+++ b/ndn-cxx/util/io.cpp
@@ -21,13 +21,20 @@
 
 #include "ndn-cxx/util/io.hpp"
 #include "ndn-cxx/encoding/buffer-stream.hpp"
-#include "ndn-cxx/security/transform.hpp"
+#include "ndn-cxx/security/transform/base64-decode.hpp"
+#include "ndn-cxx/security/transform/base64-encode.hpp"
+#include "ndn-cxx/security/transform/buffer-source.hpp"
+#include "ndn-cxx/security/transform/hex-decode.hpp"
+#include "ndn-cxx/security/transform/hex-encode.hpp"
+#include "ndn-cxx/security/transform/stream-sink.hpp"
+#include "ndn-cxx/security/transform/stream-source.hpp"
+#include "ndn-cxx/security/transform/strip-space.hpp"
 
 namespace ndn {
 namespace io {
 
-optional<Block>
-loadBlock(std::istream& is, IoEncoding encoding)
+shared_ptr<Buffer>
+loadBuffer(std::istream& is, IoEncoding encoding)
 {
   namespace t = ndn::security::transform;
 
@@ -36,55 +43,65 @@
     switch (encoding) {
       case NO_ENCODING:
         t::streamSource(is) >> t::streamSink(os);
-        break;
+        return os.buf();
       case BASE64:
         t::streamSource(is) >> t::stripSpace("\n") >> t::base64Decode(false) >> t::streamSink(os);
-        break;
+        return os.buf();
       case HEX:
         t::streamSource(is) >> t::hexDecode() >> t::streamSink(os);
-        break;
-      default:
-        return nullopt;
+        return os.buf();
     }
   }
-  catch (const t::Error&) {
-    return nullopt;
+  catch (const t::Error& e) {
+    NDN_THROW_NESTED(Error(e.what()));
   }
 
+  NDN_THROW(std::invalid_argument("Unknown IoEncoding " + to_string(encoding)));
+}
+
+optional<Block>
+loadBlock(std::istream& is, IoEncoding encoding)
+{
   try {
-    return make_optional<Block>(os.buf());
-  }
-  catch (const tlv::Error&) {
-    return nullopt;
+    return make_optional<Block>(loadBuffer(is, encoding));
   }
   catch (const std::invalid_argument&) {
     return nullopt;
   }
+  catch (const std::runtime_error&) {
+    return nullopt;
+  }
 }
 
 void
-saveBlock(const Block& block, std::ostream& os, IoEncoding encoding)
+saveBuffer(const uint8_t* buf, size_t size, std::ostream& os, IoEncoding encoding)
 {
   namespace t = ndn::security::transform;
 
   try {
     switch (encoding) {
       case NO_ENCODING:
-        t::bufferSource(block.wire(), block.size()) >> t::streamSink(os);
-        break;
+        t::bufferSource(buf, size) >> t::streamSink(os);
+        return;
       case BASE64:
-        t::bufferSource(block.wire(), block.size()) >> t::base64Encode() >> t::streamSink(os);
-        break;
+        t::bufferSource(buf, size) >> t::base64Encode() >> t::streamSink(os);
+        return;
       case HEX:
-        t::bufferSource(block.wire(), block.size()) >> t::hexEncode(true) >> t::streamSink(os);
-        break;
-      default:
-        NDN_THROW(Error("Unknown IoEncoding " + to_string(encoding)));
+        t::bufferSource(buf, size) >> t::hexEncode(true) >> t::streamSink(os);
+        return;
     }
   }
-  catch (const t::Error&) {
-    NDN_THROW_NESTED(Error("Transform error during save"));
+  catch (const t::Error& e) {
+    NDN_THROW_NESTED(Error(e.what()));
   }
+
+  NDN_THROW(std::invalid_argument("Unknown IoEncoding " + to_string(encoding)));
+}
+
+void
+saveBlock(const Block& block, std::ostream& os, IoEncoding encoding)
+{
+  saveBuffer(block.wire(), block.size(), os, encoding);
 }
 
 } // namespace io
diff --git a/ndn-cxx/util/io.hpp b/ndn-cxx/util/io.hpp
index e8f6e8d..709fe5d 100644
--- a/ndn-cxx/util/io.hpp
+++ b/ndn-cxx/util/io.hpp
@@ -36,25 +36,25 @@
   using std::runtime_error::runtime_error;
 };
 
-/** \brief indicates how a file or stream is encoded
+/** \brief Indicates how a file or stream of bytes is encoded.
  */
 enum IoEncoding {
-  /** \brief binary without encoding
+  /** \brief Raw binary, without encoding
    */
   NO_ENCODING,
 
-  /** \brief base64 encoding
+  /** \brief Base64 encoding
    *
-   *  \p save() inserts a newline after every 64 characters,
-   *  \p load() can accept base64 text with or without newlines
+   *  `save()` inserts a newline after every 64 characters,
+   *  `load()` can accept base64 text with or without newlines.
    */
   BASE64,
 
-  /** \brief hexadecimal encoding
+  /** \brief Hexadecimal encoding
    *
-   *  \p save() uses uppercase letters A-F, \p load() can accept mixed-case
+   *  `save()` uses uppercase letters A-F, `load()` can accept mixed-case.
    */
-  HEX
+  HEX,
 };
 
 namespace detail {
@@ -63,29 +63,36 @@
 static void
 checkInnerError(typename T::Error*)
 {
-  static_assert(std::is_base_of<tlv::Error, typename T::Error>::value,
-                "T::Error, if declared, must inherit from ndn::tlv::Error");
+  static_assert(std::is_convertible<typename T::Error*, tlv::Error*>::value,
+                "T::Error, if defined, must be a subclass of ndn::tlv::Error");
 }
 
 template<typename T>
 static void
 checkInnerError(...)
 {
-  // T::Error is not declared
+  // T::Error is not defined
 }
 
 } // namespace detail
 
-/** \brief loads a TLV block from a stream
- *  \return if success, the Block and true
- *          otherwise, a default-constructed Block and false
+/** \brief Reads bytes from a stream until EOF.
+ *  \return a Buffer containing the bytes read from the stream
+ *  \throw Error error during loading
+ *  \throw std::invalid_argument the specified encoding is not supported
+ */
+shared_ptr<Buffer>
+loadBuffer(std::istream& is, IoEncoding encoding = BASE64);
+
+/** \brief Reads a TLV block from a stream.
+ *  \return a Block, or nullopt if an error occurs
  */
 optional<Block>
 loadBlock(std::istream& is, IoEncoding encoding = BASE64);
 
-/** \brief loads a TLV element from a stream
- *  \tparam T type of TLV element; T must be WireDecodable,
- *            and must have a Error nested type
+/** \brief Reads a TLV element from a stream.
+ *  \tparam T type of TLV element; `T` must be WireDecodable and the nested type
+ *            `T::Error`, if defined, must be a subclass of ndn::tlv::Error
  *  \return the TLV element, or nullptr if an error occurs
  */
 template<typename T>
@@ -95,7 +102,7 @@
   BOOST_CONCEPT_ASSERT((WireDecodable<T>));
   detail::checkInnerError<T>(nullptr);
 
-  optional<Block> block = loadBlock(is, encoding);
+  auto block = loadBlock(is, encoding);
   if (!block) {
     return nullptr;
   }
@@ -108,7 +115,10 @@
   }
 }
 
-/** \brief loads a TLV element from a file
+/** \brief Reads a TLV element from a file.
+ *  \tparam T type of TLV element; `T` must be WireDecodable and the nested type
+ *            `T::Error`, if defined, must be a subclass of ndn::tlv::Error
+ *  \return the TLV element, or nullptr if an error occurs
  */
 template<typename T>
 shared_ptr<T>
@@ -118,16 +128,25 @@
   return load<T>(is, encoding);
 }
 
-/** \brief saves a TLV block to a stream
+/** \brief Writes a byte buffer to a stream.
  *  \throw Error error during saving
+ *  \throw std::invalid_argument the specified encoding is not supported
+ */
+void
+saveBuffer(const uint8_t* buf, size_t size, std::ostream& os, IoEncoding encoding = BASE64);
+
+/** \brief Writes a TLV block to a stream.
+ *  \throw Error error during saving
+ *  \throw std::invalid_argument the specified encoding is not supported
  */
 void
 saveBlock(const Block& block, std::ostream& os, IoEncoding encoding = BASE64);
 
-/** \brief saves a TLV element to a stream
- *  \tparam T type of TLV element; T must be WireEncodable,
- *            and must have a Error nested type
+/** \brief Writes a TLV element to a stream.
+ *  \tparam T type of TLV element; `T` must be WireEncodable and the nested type
+ *            `T::Error`, if defined, must be a subclass of ndn::tlv::Error
  *  \throw Error error during encoding or saving
+ *  \throw std::invalid_argument the specified encoding is not supported
  */
 template<typename T>
 void
@@ -147,7 +166,11 @@
   saveBlock(block, os, encoding);
 }
 
-/** \brief saves a TLV element to a file
+/** \brief Writes a TLV element to a file.
+ *  \tparam T type of TLV element; `T` must be WireEncodable and the nested type
+ *            `T::Error`, if defined, must be a subclass of ndn::tlv::Error
+ *  \throw Error error during encoding or saving
+ *  \throw std::invalid_argument the specified encoding is not supported
  */
 template<typename T>
 void
diff --git a/tests/unit/util/io.t.cpp b/tests/unit/util/io.t.cpp
index f8595c9..9340279 100644
--- a/tests/unit/util/io.t.cpp
+++ b/tests/unit/util/io.t.cpp
@@ -25,21 +25,88 @@
 #include "tests/identity-management-fixture.hpp"
 
 #include <boost/filesystem.hpp>
+#include <boost/mpl/vector.hpp>
 
 namespace ndn {
 namespace tests {
 
-class IoFixture
+BOOST_AUTO_TEST_SUITE(Util)
+BOOST_AUTO_TEST_SUITE(TestIo)
+
+struct NoEncoding
+{
+  const io::IoEncoding encoding{io::NO_ENCODING};
+  const std::vector<uint8_t> blob{0xd1, 0x0, 0xb0, 0x1a};
+  std::istringstream stream{std::string("\xd1\x00\xb0\x1a", 4), std::ios_base::binary};
+};
+
+struct Base64Encoding
+{
+  const io::IoEncoding encoding = io::BASE64;
+  const std::vector<uint8_t> blob{0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x45, 0x6e, 0x63};
+  std::istringstream stream{"QmFzZTY0RW5j\n", std::ios_base::binary};
+};
+
+struct HexEncoding
+{
+  const io::IoEncoding encoding = io::HEX;
+  const std::vector<uint8_t> blob{0x48, 0x65, 0x78, 0x45, 0x6e, 0x63};
+  std::istringstream stream{"486578456E63", std::ios_base::binary};
+};
+
+using Encodings = boost::mpl::vector<NoEncoding, Base64Encoding, HexEncoding>;
+
+BOOST_AUTO_TEST_CASE_TEMPLATE(LoadBuffer, T, Encodings)
+{
+  T t;
+  shared_ptr<Buffer> buf = io::loadBuffer(t.stream, t.encoding);
+  BOOST_CHECK_EQUAL_COLLECTIONS(buf->begin(), buf->end(), t.blob.begin(), t.blob.end());
+}
+
+BOOST_AUTO_TEST_CASE_TEMPLATE(SaveBuffer, T, Encodings)
+{
+  T t;
+  std::ostringstream os(std::ios_base::binary);
+  io::saveBuffer(t.blob.data(), t.blob.size(), os, t.encoding);
+  BOOST_CHECK_EQUAL(os.str(), t.stream.str());
+}
+
+BOOST_AUTO_TEST_CASE(LoadBufferException)
+{
+  std::ifstream in("this-file-does-not-exist", std::ios_base::binary);
+  BOOST_CHECK_THROW(io::loadBuffer(in, io::NO_ENCODING), io::Error);
+}
+
+BOOST_AUTO_TEST_CASE(SaveBufferException)
+{
+  class NullStreambuf : public std::streambuf
+  {
+  };
+
+  NullStreambuf nullbuf;
+  std::ostream out(&nullbuf);
+  const Buffer buffer(1);
+  BOOST_CHECK_THROW(io::saveBuffer(buffer.data(), buffer.size(), out, io::NO_ENCODING), io::Error);
+}
+
+BOOST_AUTO_TEST_CASE(UnknownIoEncoding)
+{
+  std::stringstream ss;
+  BOOST_CHECK_THROW(io::loadBuffer(ss, static_cast<io::IoEncoding>(5)), std::invalid_argument);
+  BOOST_CHECK_THROW(io::saveBuffer(nullptr, 0, ss, static_cast<io::IoEncoding>(5)), std::invalid_argument);
+}
+
+class FileIoFixture
 {
 protected:
-  IoFixture()
-    : filepath(boost::filesystem::path(UNIT_TEST_CONFIG_PATH) /= "TestIo")
+  FileIoFixture()
+    : filepath(boost::filesystem::path(UNIT_TEST_CONFIG_PATH) / "TestIo")
     , filename(filepath.string())
   {
     boost::filesystem::create_directories(filepath.parent_path());
   }
 
-  ~IoFixture()
+  ~FileIoFixture()
   {
     boost::system::error_code ec;
     boost::filesystem::remove(filepath, ec); // ignore error
@@ -53,28 +120,29 @@
     boost::filesystem::create_directory(filepath);
   }
 
-  template<typename Container, typename CharT = typename Container::value_type>
+  template<typename Container>
   Container
   readFile() const
   {
     Container container;
     std::ifstream fs(filename, std::ios_base::binary);
+    BOOST_REQUIRE_MESSAGE(fs, "error opening file");
     char ch;
     while (fs.get(ch)) {
-      container.push_back(static_cast<CharT>(ch));
+      container.push_back(static_cast<typename Container::value_type>(ch));
     }
     return container;
   }
 
-  template<typename Container, typename CharT = typename Container::value_type>
+  template<typename Container>
   void
   writeFile(const Container& content) const
   {
     std::ofstream fs(filename, std::ios_base::binary);
-    for (CharT ch : content) {
+    BOOST_REQUIRE_MESSAGE(fs, "error opening file");
+    for (auto ch : content) {
       fs.put(static_cast<char>(ch));
     }
-    fs.close();
     BOOST_REQUIRE_MESSAGE(fs, "error writing file");
   }
 
@@ -83,8 +151,7 @@
   const std::string filename;
 };
 
-BOOST_AUTO_TEST_SUITE(Util)
-BOOST_FIXTURE_TEST_SUITE(TestIo, IoFixture)
+BOOST_FIXTURE_TEST_SUITE(FileIo, FileIoFixture)
 
 class EncodableType
 {
@@ -104,7 +171,7 @@
   bool shouldThrow = false;
 };
 
-template<bool SHOULD_THROW = false>
+template<bool SHOULD_THROW>
 class DecodableTypeTpl
 {
 public:
@@ -119,7 +186,7 @@
   void
   wireDecode(const Block& block)
   {
-    if (m_shouldThrow) {
+    if (SHOULD_THROW) {
       NDN_THROW(tlv::Error("decode error"));
     }
 
@@ -128,13 +195,10 @@
     BOOST_REQUIRE_EQUAL(block.value_size(), 1);
     BOOST_CHECK_EQUAL(block.value()[0], 0xEE);
   }
-
-private:
-  bool m_shouldThrow = SHOULD_THROW;
 };
 
-typedef DecodableTypeTpl<false> DecodableType;
-typedef DecodableTypeTpl<true> DecodableTypeThrow;
+using DecodableType = DecodableTypeTpl<false>;
+using DecodableTypeThrow = DecodableTypeTpl<true>;
 
 BOOST_AUTO_TEST_CASE(LoadNoEncoding)
 {
@@ -260,7 +324,9 @@
   BOOST_CHECK_THROW(io::save(encoded, filename, io::NO_ENCODING), io::Error);
 }
 
-class IdCertFixture : public IoFixture
+BOOST_AUTO_TEST_SUITE_END() // FileIo
+
+class IdCertFixture : public FileIoFixture
                     , public IdentityManagementFixture
 {
 };