raw: Add class for extracting bytes into structures

Change-Id: I30364d9fc5a5f02ee27e6b8fd17f40226b44dcc4
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/src/meson.build b/src/meson.build
index aadb68d..11080b0 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,5 +1,54 @@
 stdplus_headers = include_directories('.')
 
+fmt_dep = dependency('fmt', required: false)
+fmt_ext = fmt_dep
+if not fmt_dep.found()
+  fmt_proj = import('cmake').subproject(
+    'fmt',
+    cmake_options: [
+      '-DCMAKE_POSITION_INDEPENDENT_CODE=ON',
+      '-DMASTER_PROJECT=OFF'
+    ],
+    required: false)
+  assert(fmt_proj.found(), 'fmtlib is required')
+  fmt_dep = fmt_proj.dependency('fmt')
+endif
+
+# span-lite might not have a pkg-config. It is header only so just make
+# sure we can access the needed symbols from the header.
+span_dep = dependency('', required: false)
+span_ext = span_dep
+has_span = meson.get_compiler('cpp').has_header_symbol(
+  'span',
+  'std::dynamic_extent',
+  dependencies: span_dep,
+  required: false)
+if not has_span
+  span_dep = dependency('span-lite', required: false)
+  span_ext = span_dep
+  has_span = meson.get_compiler('cpp').has_header_symbol(
+    'nonstd/span.hpp',
+    'nonstd::dynamic_extent',
+    dependencies: span_dep,
+    required: false)
+  if not has_span
+    span_lite_proj = import('cmake').subproject(
+      'span-lite',
+      cmake_options: [
+      ],
+      required: false)
+    if span_lite_proj.found()
+      span_dep = span_lite_proj.dependency('span-lite')
+      has_span = true
+    endif
+  endif
+endif
+
+stdplus_deps = [
+  fmt_dep,
+  span_dep,
+]
+
 stdplus_lib = library(
   'stdplus',
   [
@@ -7,10 +56,12 @@
   ],
   include_directories: stdplus_headers,
   implicit_include_directories: false,
+  dependencies: stdplus_deps,
   version: meson.project_version(),
   install: true)
 
 stdplus = declare_dependency(
+  dependencies: stdplus_deps,
   include_directories: stdplus_headers,
   link_with: stdplus_lib)
 
@@ -18,9 +69,11 @@
   name: 'stdplus',
   description: 'C++ helper utilities',
   version: meson.project_version(),
-  libraries: stdplus)
+  libraries: stdplus,
+  requires: [fmt_ext, span_ext])
 
 install_headers(
+  'stdplus/raw.hpp',
   'stdplus/signal.hpp',
   subdir: 'stdplus')
 
diff --git a/src/stdplus/raw.hpp b/src/stdplus/raw.hpp
new file mode 100644
index 0000000..1f3893f
--- /dev/null
+++ b/src/stdplus/raw.hpp
@@ -0,0 +1,137 @@
+#pragma once
+#include <fmt/format.h>
+#include <stdexcept>
+#include <stdplus/types.hpp>
+#include <string_view>
+#include <type_traits>
+
+namespace stdplus
+{
+namespace raw
+{
+
+namespace detail
+{
+
+/** @brief Determines if the container holds trivially copyable data
+ */
+template <typename Container>
+inline constexpr bool trivialContainer =
+    std::is_trivially_copyable_v<std::remove_pointer_t<decltype(
+        std::data(std::declval<std::add_lvalue_reference_t<Container>>()))>>;
+
+} // namespace detail
+
+/** @brief Copies data from a buffer into a copyable type
+ *
+ *  @param[in] data - The data buffer being copied from
+ *  @return The copyable type with data populated
+ */
+template <typename T, typename Container>
+T copyFrom(const Container& c)
+{
+    static_assert(std::is_trivially_copyable_v<T>);
+    static_assert(detail::trivialContainer<Container>);
+    T ret;
+    const size_t bytes = std::size(c) * sizeof(*std::data(c));
+    if (bytes < sizeof(ret))
+    {
+        throw std::runtime_error(
+            fmt::format("CopyFrom: {} < {}", bytes, sizeof(ret)));
+    }
+    std::memcpy(&ret, std::data(c), sizeof(ret));
+    return ret;
+}
+
+/** @brief Extracts data from a buffer into a copyable type
+ *         Updates the data buffer to show that data was removed
+ *
+ *  @param[in,out] data - The data buffer being extracted from
+ *  @return The copyable type with data populated
+ */
+template <typename T, typename CharT>
+T extract(std::basic_string_view<CharT>& data)
+{
+    T ret = copyFrom<T>(data);
+    static_assert(sizeof(T) % sizeof(CharT) == 0);
+    data.remove_prefix(sizeof(T) / sizeof(CharT));
+    return ret;
+}
+#ifdef STDPLUS_SPAN_TYPE
+template <typename T, typename IntT,
+          typename = std::enable_if_t<std::is_integral_v<IntT>>>
+T extract(span<const IntT>& data)
+{
+    T ret = copyFrom<T>(data);
+    static_assert(sizeof(T) % sizeof(IntT) == 0);
+    data = data.subspan(sizeof(T) / sizeof(IntT));
+    return ret;
+}
+#endif
+
+/** @brief Returns the span referencing the data of the raw trivial type
+ *         or of trivial types in a contiguous container.
+ *
+ *  @param[in] t - The trivial raw data
+ *  @return A view over the input with the given output integral type
+ */
+template <typename CharT, typename T,
+          typename = std::enable_if_t<std::is_trivially_copyable_v<T>>>
+std::basic_string_view<CharT> asView(const T& t) noexcept
+{
+    static_assert(sizeof(T) % sizeof(CharT) == 0);
+    return {reinterpret_cast<const CharT*>(&t), sizeof(T) / sizeof(CharT)};
+}
+template <typename CharT, typename Container,
+          typename = std::enable_if_t<!std::is_trivially_copyable_v<Container>>,
+          typename = decltype(std::data(std::declval<Container>()))>
+std::basic_string_view<CharT> asView(const Container& c) noexcept
+{
+    static_assert(detail::trivialContainer<Container>);
+    static_assert(sizeof(*std::data(c)) % sizeof(CharT) == 0);
+    return {reinterpret_cast<const CharT*>(std::data(c)),
+            std::size(c) * sizeof(*std::data(c)) / sizeof(CharT)};
+}
+#ifdef STDPLUS_SPAN_TYPE
+template <typename IntT, typename T,
+          typename = std::enable_if_t<std::is_integral_v<IntT>>,
+          typename = std::enable_if_t<std::is_trivially_copyable_v<T>>>
+span<IntT> asSpan(T& t) noexcept
+{
+    static_assert(sizeof(T) % sizeof(IntT) == 0);
+    return {reinterpret_cast<IntT*>(&t), sizeof(T) / sizeof(IntT)};
+}
+template <typename IntT, typename Container,
+          typename = std::enable_if_t<std::is_integral_v<IntT>>,
+          typename = std::enable_if_t<!std::is_trivially_copyable_v<Container>>,
+          typename = decltype(std::data(std::declval<Container>()))>
+span<IntT> asSpan(Container& c) noexcept
+{
+    static_assert(detail::trivialContainer<Container>);
+    static_assert(sizeof(*std::data(c)) % sizeof(IntT) == 0);
+    return {reinterpret_cast<IntT*>(std::data(c)),
+            std::size(c) * sizeof(*std::data(c)) / sizeof(IntT)};
+}
+template <typename IntT, typename T,
+          typename = std::enable_if_t<std::is_integral_v<IntT>>,
+          typename = std::enable_if_t<std::is_trivially_copyable_v<T>>>
+span<const IntT> asSpan(const T& t) noexcept
+{
+    static_assert(sizeof(T) % sizeof(IntT) == 0);
+    return {reinterpret_cast<const IntT*>(&t), sizeof(T) / sizeof(IntT)};
+}
+template <typename IntT, typename Container,
+          typename = std::enable_if_t<std::is_integral_v<IntT>>,
+          typename = std::enable_if_t<!std::is_trivially_copyable_v<Container>>,
+          typename = decltype(std::data(std::declval<Container>()))>
+span<const IntT> asSpan(const Container& c) noexcept
+{
+    static_assert(detail::trivialContainer<Container>);
+    static_assert(sizeof(*std::data(c)) % sizeof(IntT) == 0);
+    return {reinterpret_cast<const IntT*>(std::data(c)),
+            std::size(c) * sizeof(*std::data(c)) / sizeof(IntT)};
+}
+#endif
+
+} // namespace raw
+} // namespace stdplus
diff --git a/src/stdplus/types.hpp b/src/stdplus/types.hpp
new file mode 100644
index 0000000..5ac3ed8
--- /dev/null
+++ b/src/stdplus/types.hpp
@@ -0,0 +1,18 @@
+#pragma once
+#if defined(__cpp_lib_span) && __cpp_lib_span >= 202002L
+#include <span>
+#define STDPLUS_SPAN_TYPE std::span
+#elif __has_include(<nonstd/span.hpp>)
+#include <nonstd/span.hpp>
+#define STDPLUS_SPAN_TYPE nonstd::span
+#endif
+
+namespace stdplus
+{
+
+#ifdef STDPLUS_SPAN_TYPE
+template <typename... Args>
+using span = STDPLUS_SPAN_TYPE<Args...>;
+#endif
+
+} // namespace stdplus
diff --git a/subprojects/Catch2.wrap b/subprojects/Catch2.wrap
index ea8939c..dce6cca 100644
--- a/subprojects/Catch2.wrap
+++ b/subprojects/Catch2.wrap
@@ -1,3 +1,3 @@
 [wrap-git]
 url = https://github.com/catchorg/Catch2
-revision = v2.11.1
+revision = v2.12.2
diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap
new file mode 100644
index 0000000..bd840f4
--- /dev/null
+++ b/subprojects/fmt.wrap
@@ -0,0 +1,3 @@
+[wrap-git]
+url = https://github.com/fmtlib/fmt
+revision = 6.2.1
diff --git a/subprojects/span-lite.wrap b/subprojects/span-lite.wrap
new file mode 100644
index 0000000..f7bcddb
--- /dev/null
+++ b/subprojects/span-lite.wrap
@@ -0,0 +1,3 @@
+[wrap-git]
+url = https://github.com/martinmoene/span-lite
+revision = v0.7.0
diff --git a/test/meson.build b/test/meson.build
index 4b726d6..11eccf5 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -54,6 +54,7 @@
 
 catch2_tests = [
   'signal',
+  'raw',
 ]
 
 if has_catch2
@@ -68,6 +69,6 @@
                        build_by_default: false,
                        implicit_include_directories: false,
                        link_with: libcatch2,
-                       dependencies: [stdplus, catch2_dep]))
+                       dependencies: [stdplus, span_dep, catch2_dep]))
   endforeach
 endif
diff --git a/test/raw.cpp b/test/raw.cpp
new file mode 100644
index 0000000..81de96d
--- /dev/null
+++ b/test/raw.cpp
@@ -0,0 +1,156 @@
+#include <catch2/catch.hpp>
+#include <endian.h>
+#include <stdexcept>
+#include <stdplus/raw.hpp>
+#include <string_view>
+#include <vector>
+
+namespace stdplus
+{
+namespace raw
+{
+namespace
+{
+
+TEST_CASE("Copy From Empty", "[CopyFrom]")
+{
+    const std::string_view s;
+    CHECK_THROWS_AS(copyFrom<int>(s), std::runtime_error);
+    CHECK(s.empty());
+}
+
+TEST_CASE("Copy From Basic", "[CopyFrom]")
+{
+    int a = 4;
+    const std::string_view s(reinterpret_cast<char*>(&a), sizeof(a));
+    CHECK(a == copyFrom<int>(s));
+}
+
+TEST_CASE("Copy From Partial", "[CopyFrom]")
+{
+    const std::vector<char> s = {'a', 'b', 'c'};
+    CHECK('a' == copyFrom<char>(s));
+    const char s2[] = "def";
+    CHECK('d' == copyFrom<char>(s2));
+}
+
+TEST_CASE("Extract Too Small", "[Extract]")
+{
+    std::string_view s("a");
+    CHECK_THROWS_AS(extract<int>(s), std::runtime_error);
+    CHECK(1 == s.size());
+}
+
+TEST_CASE("Extract Basic", "[Extract]")
+{
+    int a = 4;
+    std::string_view s(reinterpret_cast<char*>(&a), sizeof(a));
+    CHECK(a == extract<int>(s));
+    CHECK(s.empty());
+}
+
+TEST_CASE("Extract Partial", "[Extract]")
+{
+    std::string_view s("abc");
+    CHECK('a' == extract<char>(s));
+    CHECK(2 == s.size());
+}
+
+TEST_CASE("As View Byte", "[AsView]")
+{
+    int32_t a = 4;
+    auto s = asView<uint8_t>(a);
+    CHECK(a == copyFrom<int>(s));
+}
+
+TEST_CASE("As View Int", "[AsView]")
+{
+    int32_t a = 4;
+    auto s = asView<char16_t>(a);
+    CHECK(a == copyFrom<int>(s));
+}
+
+TEST_CASE("As View Arr", "[AsView]")
+{
+    std::vector<uint32_t> arr = {htole32(1), htole32(2)};
+    auto s = asView<char16_t>(arr);
+    REQUIRE(4 == s.size());
+    CHECK(htole16(1) == s[0]);
+    CHECK(htole16(0) == s[1]);
+    CHECK(htole16(2) == s[2]);
+    CHECK(htole16(0) == s[3]);
+}
+
+#ifdef STDPLUS_SPAN_TYPE
+TEST_CASE("Span Extract TooSmall", "[Extract]")
+{
+    const std::vector<char> v = {'c'};
+    span<const char> s = v;
+    CHECK_THROWS_AS(extract<int>(s), std::runtime_error);
+    CHECK(1 == s.size());
+}
+
+TEST_CASE("Span Extract Basic", "[Extract]")
+{
+    const std::vector<int> v = {4};
+    span<const int> s = v;
+    CHECK(v[0] == extract<int>(s));
+    CHECK(s.empty());
+}
+
+TEST_CASE("Span Extract Larger", "[Extract]")
+{
+    const std::vector<int> v{3, 4, 5};
+    span<const int> s = v;
+    CHECK(v[0] == extract<int>(s));
+    CHECK(v.size() - 1 == s.size());
+}
+
+TEST_CASE("As Span const", "[AsSpan]")
+{
+    const uint64_t data = htole64(0xffff0000);
+    auto s = asSpan<uint32_t>(data);
+    CHECK(s.size() == 2);
+    CHECK(s[0] == htole32(0xffff0000));
+    CHECK(s[1] == htole32(0x00000000));
+}
+
+TEST_CASE("As Span Arr const", "[AsSpan]")
+{
+    const std::vector<uint32_t> arr = {htole32(1), htole32(2)};
+    auto s = asSpan<uint16_t>(arr);
+    REQUIRE(4 == s.size());
+    CHECK(htole16(1) == s[0]);
+    CHECK(htole16(0) == s[1]);
+    CHECK(htole16(2) == s[2]);
+    CHECK(htole16(0) == s[3]);
+}
+
+TEST_CASE("As Span", "[AsSpan]")
+{
+    uint64_t data = htole64(0xffff0000);
+    auto s = asSpan<uint16_t>(data);
+    CHECK(s.size() == 4);
+    s[2] = 0xfefe;
+    CHECK(s[0] == htole16(0x0000));
+    CHECK(s[1] == htole16(0xffff));
+    CHECK(s[2] == htole16(0xfefe));
+    CHECK(s[3] == htole16(0x0000));
+}
+
+TEST_CASE("As Span Arr", "[AsSpan]")
+{
+    std::vector<uint32_t> arr = {htole32(1), htole32(2)};
+    auto s = asSpan<uint16_t>(arr);
+    REQUIRE(4 == s.size());
+    CHECK(htole16(1) == s[0]);
+    CHECK(htole16(0) == s[1]);
+    CHECK(htole16(2) == s[2]);
+    CHECK(htole16(0) == s[3]);
+}
+
+#endif
+
+} // namespace
+} // namespace raw
+} // namespace stdplus