dcsctp: Add TLV trait

Various entities in SCTP are padded data blocks, with a type and
length field at fixed offsets, all stored in a 4-byte header. This is
called the Type-Length-Value format, or TLV for short.

See e.g. https://tools.ietf.org/html/rfc4960#section-3.2 and
https://tools.ietf.org/html/rfc4960#section-3.2.1

This templated class, which is used as a trait[1], is configurable -
a struct passed in as template parameter.

[1] https://en.wikipedia.org/wiki/Trait_(computer_programming)

Bug: webrtc:12614
Change-Id: I52c2b5056931aba5fb23419406314136b5a4f650
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/213180
Commit-Queue: Victor Boivie <boivie@webrtc.org>
Reviewed-by: Tommi <tommi@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#33602}
diff --git a/net/dcsctp/packet/BUILD.gn b/net/dcsctp/packet/BUILD.gn
index d546726..41e024a 100644
--- a/net/dcsctp/packet/BUILD.gn
+++ b/net/dcsctp/packet/BUILD.gn
@@ -25,12 +25,28 @@
   ]
 }
 
+rtc_library("tlv_trait") {
+  deps = [
+    ":bounded_io",
+    "../../../api:array_view",
+    "../../../rtc_base",
+    "../../../rtc_base:checks",
+    "../../../rtc_base:rtc_base_approved",
+  ]
+  absl_deps = [ "//third_party/abseil-cpp/absl/strings:strings" ]
+  sources = [
+    "tlv_trait.cc",
+    "tlv_trait.h",
+  ]
+}
+
 if (rtc_include_tests) {
   rtc_library("dcsctp_packet_unittests") {
     testonly = true
 
     deps = [
       ":bounded_io",
+      ":tlv_trait",
       "../../../api:array_view",
       "../../../rtc_base:checks",
       "../../../rtc_base:gunit_helpers",
@@ -40,6 +56,7 @@
     sources = [
       "bounded_byte_reader_test.cc",
       "bounded_byte_writer_test.cc",
+      "tlv_trait_test.cc",
     ]
   }
 }
diff --git a/net/dcsctp/packet/tlv_trait.cc b/net/dcsctp/packet/tlv_trait.cc
new file mode 100644
index 0000000..493b6a4
--- /dev/null
+++ b/net/dcsctp/packet/tlv_trait.cc
@@ -0,0 +1,46 @@
+/*
+ *  Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "net/dcsctp/packet/tlv_trait.h"
+
+#include "rtc_base/logging.h"
+
+namespace dcsctp {
+namespace tlv_trait_impl {
+void ReportInvalidSize(size_t actual_size, size_t expected_size) {
+  RTC_DLOG(LS_WARNING) << "Invalid size (" << actual_size
+                       << ", expected minimum " << expected_size << " bytes)";
+}
+
+void ReportInvalidType(int actual_type, int expected_type) {
+  RTC_DLOG(LS_WARNING) << "Invalid type (" << actual_type << ", expected "
+                       << expected_type << ")";
+}
+
+void ReportInvalidFixedLengthField(size_t value, size_t expected) {
+  RTC_DLOG(LS_WARNING) << "Invalid length field (" << value << ", expected "
+                       << expected << " bytes)";
+}
+
+void ReportInvalidVariableLengthField(size_t value, size_t available) {
+  RTC_DLOG(LS_WARNING) << "Invalid length field (" << value << ", available "
+                       << available << " bytes)";
+}
+
+void ReportInvalidPadding(size_t padding_bytes) {
+  RTC_DLOG(LS_WARNING) << "Invalid padding (" << padding_bytes << " bytes)";
+}
+
+void ReportInvalidLengthMultiple(size_t length, size_t alignment) {
+  RTC_DLOG(LS_WARNING) << "Invalid length field (" << length
+                       << ", expected an even multiple of " << alignment
+                       << " bytes)";
+}
+}  // namespace tlv_trait_impl
+}  // namespace dcsctp
diff --git a/net/dcsctp/packet/tlv_trait.h b/net/dcsctp/packet/tlv_trait.h
new file mode 100644
index 0000000..9cdb924
--- /dev/null
+++ b/net/dcsctp/packet/tlv_trait.h
@@ -0,0 +1,154 @@
+/*
+ *  Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef NET_DCSCTP_PACKET_TLV_TRAIT_H_
+#define NET_DCSCTP_PACKET_TLV_TRAIT_H_
+
+#include <stdint.h>
+#include <string.h>
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include "absl/types/optional.h"
+#include "api/array_view.h"
+#include "net/dcsctp/packet/bounded_byte_reader.h"
+#include "net/dcsctp/packet/bounded_byte_writer.h"
+
+namespace dcsctp {
+namespace tlv_trait_impl {
+// Logging functions, only to be used by TLVTrait, which is a templated class.
+void ReportInvalidSize(size_t actual_size, size_t expected_size);
+void ReportInvalidType(int acutal_type, int expected_type);
+void ReportInvalidFixedLengthField(size_t value, size_t expected);
+void ReportInvalidVariableLengthField(size_t value, size_t available);
+void ReportInvalidPadding(size_t padding_bytes);
+void ReportInvalidLengthMultiple(size_t length, size_t alignment);
+}  // namespace tlv_trait_impl
+
+// Various entities in SCTP are padded data blocks, with a type and length
+// field at fixed offsets, all stored in a 4-byte header.
+//
+// See e.g. https://tools.ietf.org/html/rfc4960#section-3.2 and
+// https://tools.ietf.org/html/rfc4960#section-3.2.1
+//
+// These are helper classes for writing and parsing that data, which in SCTP is
+// called Type-Length-Value, or TLV.
+//
+// This templated class is configurable - a struct passed in as template
+// parameter with the following expected members:
+//   * kType                    - The type field's value
+//   * kTypeSizeInBytes         - The type field's width in bytes.
+//                                Either 1 or 2.
+//   * kHeaderSize              - The fixed size header
+//   * kVariableLengthAlignment - The size alignment on the variable data. Set
+//                                to zero (0) if no variable data is used.
+//
+// This class is to be used as a trait
+// (https://en.wikipedia.org/wiki/Trait_(computer_programming)) that adds a few
+// public and protected members and which a class inherits from when it
+// represents a type-length-value object.
+template <typename Config>
+class TLVTrait {
+ private:
+  static constexpr size_t kTlvHeaderSize = 4;
+
+ protected:
+  static constexpr size_t kHeaderSize = Config::kHeaderSize;
+
+  static_assert(Config::kTypeSizeInBytes == 1 || Config::kTypeSizeInBytes == 2,
+                "kTypeSizeInBytes must be 1 or 2");
+  static_assert(Config::kHeaderSize >= kTlvHeaderSize,
+                "HeaderSize must be >= 4 bytes");
+  static_assert((Config::kHeaderSize % 4 == 0),
+                "kHeaderSize must be an even multiple of 4 bytes");
+  static_assert((Config::kVariableLengthAlignment == 0 ||
+                 Config::kVariableLengthAlignment == 1 ||
+                 Config::kVariableLengthAlignment == 2 ||
+                 Config::kVariableLengthAlignment == 4 ||
+                 Config::kVariableLengthAlignment == 8),
+                "kVariableLengthAlignment must be an allowed value");
+
+  // Validates the data with regards to size, alignment and type.
+  // If valid, returns a bounded buffer.
+  static absl::optional<BoundedByteReader<Config::kHeaderSize>> ParseTLV(
+      rtc::ArrayView<const uint8_t> data) {
+    if (data.size() < Config::kHeaderSize) {
+      tlv_trait_impl::ReportInvalidSize(data.size(), Config::kHeaderSize);
+      return absl::nullopt;
+    }
+    BoundedByteReader<kTlvHeaderSize> tlv_header(data);
+
+    const int type = (Config::kTypeSizeInBytes == 1) ? tlv_header.Load8<0>()
+                                                     : tlv_header.Load16<0>();
+
+    if (type != Config::kType) {
+      tlv_trait_impl::ReportInvalidType(type, Config::kType);
+      return absl::nullopt;
+    }
+    const uint16_t length = tlv_header.Load16<2>();
+    if (Config::kVariableLengthAlignment == 0) {
+      // Don't expect any variable length data at all.
+      if (length != Config::kHeaderSize || data.size() != Config::kHeaderSize) {
+        tlv_trait_impl::ReportInvalidFixedLengthField(length,
+                                                      Config::kHeaderSize);
+        return absl::nullopt;
+      }
+    } else {
+      // Expect variable length data - verify its size alignment.
+      if (length > data.size()) {
+        tlv_trait_impl::ReportInvalidVariableLengthField(length, data.size());
+        return absl::nullopt;
+      }
+      const size_t padding = data.size() - length;
+      if (padding > 3) {
+        // https://tools.ietf.org/html/rfc4960#section-3.2
+        // "This padding MUST NOT be more than 3 bytes in total"
+        tlv_trait_impl::ReportInvalidPadding(padding);
+        return absl::nullopt;
+      }
+      if ((length % Config::kVariableLengthAlignment) != 0) {
+        tlv_trait_impl::ReportInvalidLengthMultiple(
+            length, Config::kVariableLengthAlignment);
+        return absl::nullopt;
+      }
+    }
+    return BoundedByteReader<Config::kHeaderSize>(data.subview(0, length));
+  }
+
+  // Allocates space for data with a static header size, as defined by
+  // `Config::kHeaderSize` and a variable footer, as defined by `variable_size`
+  // (which may be 0) and writes the type and length in the header.
+  static BoundedByteWriter<Config::kHeaderSize> AllocateTLV(
+      std::vector<uint8_t>& out,
+      size_t variable_size = 0) {
+    const size_t offset = out.size();
+    const size_t size = Config::kHeaderSize + variable_size;
+    out.resize(offset + size);
+
+    BoundedByteWriter<kTlvHeaderSize> tlv_header(
+        rtc::ArrayView<uint8_t>(out.data() + offset, kTlvHeaderSize));
+    if (Config::kTypeSizeInBytes == 1) {
+      tlv_header.Store8<0>(static_cast<uint8_t>(Config::kType));
+    } else {
+      tlv_header.Store16<0>(Config::kType);
+    }
+    tlv_header.Store16<2>(size);
+
+    return BoundedByteWriter<Config::kHeaderSize>(
+        rtc::ArrayView<uint8_t>(out.data() + offset, size));
+  }
+};
+
+}  // namespace dcsctp
+
+#endif  // NET_DCSCTP_PACKET_TLV_TRAIT_H_
diff --git a/net/dcsctp/packet/tlv_trait_test.cc b/net/dcsctp/packet/tlv_trait_test.cc
new file mode 100644
index 0000000..413c71e4
--- /dev/null
+++ b/net/dcsctp/packet/tlv_trait_test.cc
@@ -0,0 +1,126 @@
+/*
+ *  Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "net/dcsctp/packet/tlv_trait.h"
+
+#include <vector>
+
+#include "api/array_view.h"
+#include "rtc_base/buffer.h"
+#include "rtc_base/checks.h"
+#include "rtc_base/gunit.h"
+#include "test/gmock.h"
+
+namespace dcsctp {
+namespace {
+using ::testing::ElementsAre;
+using ::testing::SizeIs;
+
+struct OneByteTypeConfig {
+  static constexpr int kTypeSizeInBytes = 1;
+  static constexpr int kType = 0x49;
+  static constexpr size_t kHeaderSize = 12;
+  static constexpr int kVariableLengthAlignment = 4;
+};
+
+class OneByteChunk : public TLVTrait<OneByteTypeConfig> {
+ public:
+  static constexpr size_t kVariableSize = 4;
+
+  void SerializeTo(std::vector<uint8_t>& out) {
+    BoundedByteWriter<OneByteTypeConfig::kHeaderSize> writer =
+        AllocateTLV(out, kVariableSize);
+    writer.Store32<4>(0x01020304);
+    writer.Store16<8>(0x0506);
+    writer.Store16<10>(0x0708);
+
+    uint8_t variable_data[kVariableSize] = {0xDE, 0xAD, 0xBE, 0xEF};
+    writer.CopyToVariableData(rtc::ArrayView<const uint8_t>(variable_data));
+  }
+
+  static absl::optional<BoundedByteReader<OneByteTypeConfig::kHeaderSize>>
+  Parse(rtc::ArrayView<const uint8_t> data) {
+    return ParseTLV(data);
+  }
+};
+
+TEST(TlvDataTest, CanWriteOneByteTypeTlvs) {
+  std::vector<uint8_t> out;
+  OneByteChunk().SerializeTo(out);
+
+  EXPECT_THAT(out, SizeIs(OneByteTypeConfig::kHeaderSize +
+                          OneByteChunk::kVariableSize));
+  EXPECT_THAT(out, ElementsAre(0x49, 0x00, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04,
+                               0x05, 0x06, 0x07, 0x08, 0xDE, 0xAD, 0xBE, 0xEF));
+}
+
+TEST(TlvDataTest, CanReadOneByteTypeTlvs) {
+  uint8_t data[] = {0x49, 0x00, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04,
+                    0x05, 0x06, 0x07, 0x08, 0xDE, 0xAD, 0xBE, 0xEF};
+
+  absl::optional<BoundedByteReader<OneByteTypeConfig::kHeaderSize>> reader =
+      OneByteChunk::Parse(data);
+  ASSERT_TRUE(reader.has_value());
+  EXPECT_EQ(reader->Load32<4>(), 0x01020304U);
+  EXPECT_EQ(reader->Load16<8>(), 0x0506U);
+  EXPECT_EQ(reader->Load16<10>(), 0x0708U);
+  EXPECT_THAT(reader->variable_data(), ElementsAre(0xDE, 0xAD, 0xBE, 0xEF));
+}
+
+struct TwoByteTypeConfig {
+  static constexpr int kTypeSizeInBytes = 2;
+  static constexpr int kType = 31337;
+  static constexpr size_t kHeaderSize = 8;
+  static constexpr int kVariableLengthAlignment = 4;
+};
+
+class TwoByteChunk : public TLVTrait<TwoByteTypeConfig> {
+ public:
+  static constexpr size_t kVariableSize = 8;
+
+  void SerializeTo(std::vector<uint8_t>& out) {
+    BoundedByteWriter<TwoByteTypeConfig::kHeaderSize> writer =
+        AllocateTLV(out, kVariableSize);
+    writer.Store32<4>(0x01020304U);
+
+    uint8_t variable_data[] = {0x05, 0x06, 0x07, 0x08, 0xDE, 0xAD, 0xBE, 0xEF};
+    writer.CopyToVariableData(rtc::ArrayView<const uint8_t>(variable_data));
+  }
+
+  static absl::optional<BoundedByteReader<TwoByteTypeConfig::kHeaderSize>>
+  Parse(rtc::ArrayView<const uint8_t> data) {
+    return ParseTLV(data);
+  }
+};
+
+TEST(TlvDataTest, CanWriteTwoByteTypeTlvs) {
+  std::vector<uint8_t> out;
+
+  TwoByteChunk().SerializeTo(out);
+
+  EXPECT_THAT(out, SizeIs(TwoByteTypeConfig::kHeaderSize +
+                          TwoByteChunk::kVariableSize));
+  EXPECT_THAT(out, ElementsAre(0x7A, 0x69, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04,
+                               0x05, 0x06, 0x07, 0x08, 0xDE, 0xAD, 0xBE, 0xEF));
+}
+
+TEST(TlvDataTest, CanReadTwoByteTypeTlvs) {
+  uint8_t data[] = {0x7A, 0x69, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04,
+                    0x05, 0x06, 0x07, 0x08, 0xDE, 0xAD, 0xBE, 0xEF};
+
+  absl::optional<BoundedByteReader<TwoByteTypeConfig::kHeaderSize>> reader =
+      TwoByteChunk::Parse(data);
+  EXPECT_TRUE(reader.has_value());
+  EXPECT_EQ(reader->Load32<4>(), 0x01020304U);
+  EXPECT_THAT(reader->variable_data(),
+              ElementsAre(0x05, 0x06, 0x07, 0x08, 0xDE, 0xAD, 0xBE, 0xEF));
+}
+
+}  // namespace
+}  // namespace dcsctp