dcsctp: Added common utilities

These are quite generic utilities that are used by multiple modules
within dcSCTP. Some would be good to have in rtc_base and are simple
replicas of utilities available in abseil.

Bug: webrtc:12614
Change-Id: I9914286ced7317a34628a71697da9149d6d19d38
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/213190
Reviewed-by: Tommi <tommi@webrtc.org>
Commit-Queue: Victor Boivie <boivie@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#33609}
diff --git a/net/dcsctp/BUILD.gn b/net/dcsctp/BUILD.gn
index 0ccc897..ee15a45 100644
--- a/net/dcsctp/BUILD.gn
+++ b/net/dcsctp/BUILD.gn
@@ -13,6 +13,7 @@
     testonly = true
     deps = [
       "../../test:test_main",
+      "common:dcsctp_common_unittests",
       "packet:dcsctp_packet_unittests",
     ]
   }
diff --git a/net/dcsctp/common/BUILD.gn b/net/dcsctp/common/BUILD.gn
new file mode 100644
index 0000000..55fa86b
--- /dev/null
+++ b/net/dcsctp/common/BUILD.gn
@@ -0,0 +1,55 @@
+# 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.
+
+import("//build/config/linux/pkg_config.gni")
+import("../../../webrtc.gni")
+
+rtc_source_set("math") {
+  deps = []
+  sources = [ "math.h" ]
+}
+
+rtc_source_set("pair_hash") {
+  deps = []
+  sources = [ "pair_hash.h" ]
+}
+
+rtc_source_set("sequence_numbers") {
+  deps = []
+  sources = [ "sequence_numbers.h" ]
+}
+
+rtc_source_set("str_join") {
+  deps = []
+  sources = [ "str_join.h" ]
+}
+
+if (rtc_include_tests) {
+  rtc_library("dcsctp_common_unittests") {
+    testonly = true
+
+    defines = []
+    deps = [
+      ":math",
+      ":pair_hash",
+      ":sequence_numbers",
+      ":str_join",
+      "../../../api:array_view",
+      "../../../rtc_base:checks",
+      "../../../rtc_base:gunit_helpers",
+      "../../../rtc_base:rtc_base_approved",
+      "../../../test:test_support",
+    ]
+    sources = [
+      "math_test.cc",
+      "pair_hash_test.cc",
+      "sequence_numbers_test.cc",
+      "str_join_test.cc",
+    ]
+  }
+}
diff --git a/net/dcsctp/common/math.h b/net/dcsctp/common/math.h
new file mode 100644
index 0000000..ee161d2
--- /dev/null
+++ b/net/dcsctp/common/math.h
@@ -0,0 +1,24 @@
+/*
+ *  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_COMMON_MATH_H_
+#define NET_DCSCTP_COMMON_MATH_H_
+
+namespace dcsctp {
+
+// Rounds up `val` to the nearest value that is divisible by four. Frequently
+// used to e.g. pad chunks or parameters to an even 32-bit offset.
+template <typename IntType>
+IntType RoundUpTo4(IntType val) {
+  return (val + 3) & -4;
+}
+
+}  // namespace dcsctp
+
+#endif  // NET_DCSCTP_COMMON_MATH_H_
diff --git a/net/dcsctp/common/math_test.cc b/net/dcsctp/common/math_test.cc
new file mode 100644
index 0000000..902aefa
--- /dev/null
+++ b/net/dcsctp/common/math_test.cc
@@ -0,0 +1,32 @@
+/*
+ *  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/common/math.h"
+
+#include "test/gmock.h"
+
+namespace dcsctp {
+namespace {
+
+TEST(MathUtilTest, CanRoundUpTo4) {
+  EXPECT_EQ(RoundUpTo4(0), 0);
+  EXPECT_EQ(RoundUpTo4(1), 4);
+  EXPECT_EQ(RoundUpTo4(2), 4);
+  EXPECT_EQ(RoundUpTo4(3), 4);
+  EXPECT_EQ(RoundUpTo4(4), 4);
+  EXPECT_EQ(RoundUpTo4(5), 8);
+  EXPECT_EQ(RoundUpTo4(6), 8);
+  EXPECT_EQ(RoundUpTo4(7), 8);
+  EXPECT_EQ(RoundUpTo4(8), 8);
+  EXPECT_EQ(RoundUpTo4(10000000000), 10000000000);
+  EXPECT_EQ(RoundUpTo4(10000000001), 10000000004);
+}
+
+}  // namespace
+}  // namespace dcsctp
diff --git a/net/dcsctp/common/pair_hash.h b/net/dcsctp/common/pair_hash.h
new file mode 100644
index 0000000..62af8b4
--- /dev/null
+++ b/net/dcsctp/common/pair_hash.h
@@ -0,0 +1,31 @@
+/*
+ *  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_COMMON_PAIR_HASH_H_
+#define NET_DCSCTP_COMMON_PAIR_HASH_H_
+
+#include <stddef.h>
+
+#include <functional>
+#include <utility>
+
+namespace dcsctp {
+
+// A custom hash function for std::pair, to be able to be used as key in a
+// std::unordered_map. If absl::flat_hash_map would ever be used, this is
+// unnecessary as it already has a hash function for std::pair.
+struct PairHash {
+  template <class T1, class T2>
+  size_t operator()(const std::pair<T1, T2>& p) const {
+    return (3 * std::hash<T1>{}(p.first)) ^ std::hash<T2>{}(p.second);
+  }
+};
+}  // namespace dcsctp
+
+#endif  // NET_DCSCTP_COMMON_PAIR_HASH_H_
diff --git a/net/dcsctp/common/pair_hash_test.cc b/net/dcsctp/common/pair_hash_test.cc
new file mode 100644
index 0000000..bcc3ec8
--- /dev/null
+++ b/net/dcsctp/common/pair_hash_test.cc
@@ -0,0 +1,48 @@
+/*
+ *  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/common/pair_hash.h"
+
+#include <unordered_map>
+#include <unordered_set>
+
+#include "test/gmock.h"
+
+namespace dcsctp {
+namespace {
+
+TEST(PairHashTest, CanInsertIntoSet) {
+  using MyPair = std::pair<int, int>;
+
+  std::unordered_set<MyPair, PairHash> pairs;
+
+  pairs.insert({1, 2});
+  pairs.insert({3, 4});
+
+  EXPECT_NE(pairs.find({1, 2}), pairs.end());
+  EXPECT_NE(pairs.find({3, 4}), pairs.end());
+  EXPECT_EQ(pairs.find({1, 3}), pairs.end());
+  EXPECT_EQ(pairs.find({3, 3}), pairs.end());
+}
+
+TEST(PairHashTest, CanInsertIntoMap) {
+  using MyPair = std::pair<int, int>;
+
+  std::unordered_map<MyPair, int, PairHash> pairs;
+
+  pairs[{1, 2}] = 99;
+  pairs[{3, 4}] = 100;
+
+  EXPECT_EQ((pairs[{1, 2}]), 99);
+  EXPECT_EQ((pairs[{3, 4}]), 100);
+  EXPECT_EQ(pairs.find({1, 3}), pairs.end());
+  EXPECT_EQ(pairs.find({3, 3}), pairs.end());
+}
+}  // namespace
+}  // namespace dcsctp
diff --git a/net/dcsctp/common/sequence_numbers.h b/net/dcsctp/common/sequence_numbers.h
new file mode 100644
index 0000000..f604337
--- /dev/null
+++ b/net/dcsctp/common/sequence_numbers.h
@@ -0,0 +1,150 @@
+/*
+ *  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_COMMON_SEQUENCE_NUMBERS_H_
+#define NET_DCSCTP_COMMON_SEQUENCE_NUMBERS_H_
+
+#include <cstdint>
+#include <limits>
+#include <utility>
+
+namespace dcsctp {
+
+// UnwrappedSequenceNumber handles wrapping sequence numbers and unwraps them to
+// an int64_t value space, to allow wrapped sequence numbers to be easily
+// compared for ordering.
+//
+// Sequence numbers are expected to be monotonically increasing, but they do not
+// need to be unwrapped in order, as long as the difference to the previous one
+// is not larger than half the range of the wrapped sequence number.
+template <typename WrappedType>
+class UnwrappedSequenceNumber {
+ public:
+  static_assert(!std::numeric_limits<WrappedType>::is_signed,
+                "The wrapped type must be unsigned");
+  static_assert(std::numeric_limits<WrappedType>::max() <
+                    std::numeric_limits<int64_t>::max(),
+                "The wrapped type must be less than the int64_t value space");
+
+  // The unwrapper is a sort of factory and converts wrapped sequence numbers to
+  // unwrapped ones.
+  class Unwrapper {
+   public:
+    Unwrapper() : largest_(kValueLimit) {}
+    Unwrapper(const Unwrapper&) = default;
+    Unwrapper& operator=(const Unwrapper&) = default;
+
+    // Given a wrapped `value`, and with knowledge of its current last seen
+    // largest number, will return a value that can be compared using normal
+    // operators, such as less-than, greater-than etc.
+    //
+    // This will also update the Unwrapper's state, to track the last seen
+    // largest value.
+    UnwrappedSequenceNumber<WrappedType> Unwrap(WrappedType value) {
+      WrappedType wrapped_largest =
+          static_cast<WrappedType>(largest_ % kValueLimit);
+      int64_t result = largest_ + Delta(value, wrapped_largest);
+      if (largest_ < result) {
+        largest_ = result;
+      }
+      return UnwrappedSequenceNumber<WrappedType>(result);
+    }
+
+    // Similar to `Unwrap`, but will not update the Unwrappers's internal state.
+    UnwrappedSequenceNumber<WrappedType> PeekUnwrap(WrappedType value) const {
+      WrappedType uint32_largest =
+          static_cast<WrappedType>(largest_ % kValueLimit);
+      int64_t result = largest_ + Delta(value, uint32_largest);
+      return UnwrappedSequenceNumber<WrappedType>(result);
+    }
+
+    // Resets the Unwrapper to its pristine state. Used when a sequence number
+    // is to be reset to zero.
+    void Reset() { largest_ = kValueLimit; }
+
+   private:
+    static int64_t Delta(WrappedType value, WrappedType prev_value) {
+      static constexpr WrappedType kBreakpoint = kValueLimit / 2;
+      WrappedType diff = value - prev_value;
+      diff %= kValueLimit;
+      if (diff < kBreakpoint) {
+        return static_cast<int64_t>(diff);
+      }
+      return static_cast<int64_t>(diff) - kValueLimit;
+    }
+
+    int64_t largest_;
+  };
+
+  // Returns the wrapped value this type represents.
+  WrappedType Wrap() const {
+    return static_cast<WrappedType>(value_ % kValueLimit);
+  }
+
+  template <typename H>
+  friend H AbslHashValue(H state,
+                         const UnwrappedSequenceNumber<WrappedType>& hash) {
+    return H::combine(std::move(state), hash.value_);
+  }
+
+  bool operator==(const UnwrappedSequenceNumber<WrappedType>& other) const {
+    return value_ == other.value_;
+  }
+  bool operator!=(const UnwrappedSequenceNumber<WrappedType>& other) const {
+    return value_ != other.value_;
+  }
+  bool operator<(const UnwrappedSequenceNumber<WrappedType>& other) const {
+    return value_ < other.value_;
+  }
+  bool operator>(const UnwrappedSequenceNumber<WrappedType>& other) const {
+    return value_ > other.value_;
+  }
+  bool operator>=(const UnwrappedSequenceNumber<WrappedType>& other) const {
+    return value_ >= other.value_;
+  }
+  bool operator<=(const UnwrappedSequenceNumber<WrappedType>& other) const {
+    return value_ <= other.value_;
+  }
+
+  // Increments the value.
+  void Increment() { ++value_; }
+  UnwrappedSequenceNumber<WrappedType> next_value() const {
+    return UnwrappedSequenceNumber<WrappedType>(value_ + 1);
+  }
+
+  // Adds a delta to the current value.
+  UnwrappedSequenceNumber<WrappedType> AddTo(int delta) const {
+    return UnwrappedSequenceNumber<WrappedType>(value_ + delta);
+  }
+
+  // Compares the difference between two sequence numbers.
+  WrappedType Difference(UnwrappedSequenceNumber<WrappedType> other) const {
+    return value_ - other.value_;
+  }
+
+ private:
+  explicit UnwrappedSequenceNumber(int64_t value) : value_(value) {}
+  static constexpr int64_t kValueLimit =
+      static_cast<int64_t>(1) << std::numeric_limits<WrappedType>::digits;
+
+  int64_t value_;
+};
+
+// Transmission Sequence Numbers (TSN)
+using TSN = UnwrappedSequenceNumber<uint32_t>;
+
+// Stream Sequence Numbers (SSN)
+using SSN = UnwrappedSequenceNumber<uint16_t>;
+
+// Message Identifier (MID)
+using MID = UnwrappedSequenceNumber<uint32_t>;
+
+}  // namespace dcsctp
+
+#endif  // NET_DCSCTP_COMMON_SEQUENCE_NUMBERS_H_
diff --git a/net/dcsctp/common/sequence_numbers_test.cc b/net/dcsctp/common/sequence_numbers_test.cc
new file mode 100644
index 0000000..f7ecd9b
--- /dev/null
+++ b/net/dcsctp/common/sequence_numbers_test.cc
@@ -0,0 +1,186 @@
+/*
+ *  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/common/sequence_numbers.h"
+
+#include "test/gmock.h"
+
+namespace dcsctp {
+namespace {
+
+using TestSequence = UnwrappedSequenceNumber<uint16_t>;
+
+TEST(SequenceNumbersTest, SimpleUnwrapping) {
+  TestSequence::Unwrapper unwrapper;
+
+  TestSequence s0 = unwrapper.Unwrap(0);
+  TestSequence s1 = unwrapper.Unwrap(1);
+  TestSequence s2 = unwrapper.Unwrap(2);
+  TestSequence s3 = unwrapper.Unwrap(3);
+
+  EXPECT_LT(s0, s1);
+  EXPECT_LT(s0, s2);
+  EXPECT_LT(s0, s3);
+  EXPECT_LT(s1, s2);
+  EXPECT_LT(s1, s3);
+  EXPECT_LT(s2, s3);
+
+  EXPECT_EQ(s1.Difference(s0), 1);
+  EXPECT_EQ(s2.Difference(s0), 2);
+  EXPECT_EQ(s3.Difference(s0), 3);
+
+  EXPECT_GT(s1, s0);
+  EXPECT_GT(s2, s0);
+  EXPECT_GT(s3, s0);
+  EXPECT_GT(s2, s1);
+  EXPECT_GT(s3, s1);
+  EXPECT_GT(s3, s2);
+
+  s0.Increment();
+  EXPECT_EQ(s0, s1);
+  s1.Increment();
+  EXPECT_EQ(s1, s2);
+  s2.Increment();
+  EXPECT_EQ(s2, s3);
+
+  EXPECT_EQ(s0.AddTo(2), s3);
+}
+
+TEST(SequenceNumbersTest, MidValueUnwrapping) {
+  TestSequence::Unwrapper unwrapper;
+
+  TestSequence s0 = unwrapper.Unwrap(0x7FFE);
+  TestSequence s1 = unwrapper.Unwrap(0x7FFF);
+  TestSequence s2 = unwrapper.Unwrap(0x8000);
+  TestSequence s3 = unwrapper.Unwrap(0x8001);
+
+  EXPECT_LT(s0, s1);
+  EXPECT_LT(s0, s2);
+  EXPECT_LT(s0, s3);
+  EXPECT_LT(s1, s2);
+  EXPECT_LT(s1, s3);
+  EXPECT_LT(s2, s3);
+
+  EXPECT_EQ(s1.Difference(s0), 1);
+  EXPECT_EQ(s2.Difference(s0), 2);
+  EXPECT_EQ(s3.Difference(s0), 3);
+
+  EXPECT_GT(s1, s0);
+  EXPECT_GT(s2, s0);
+  EXPECT_GT(s3, s0);
+  EXPECT_GT(s2, s1);
+  EXPECT_GT(s3, s1);
+  EXPECT_GT(s3, s2);
+
+  s0.Increment();
+  EXPECT_EQ(s0, s1);
+  s1.Increment();
+  EXPECT_EQ(s1, s2);
+  s2.Increment();
+  EXPECT_EQ(s2, s3);
+
+  EXPECT_EQ(s0.AddTo(2), s3);
+}
+
+TEST(SequenceNumbersTest, WrappedUnwrapping) {
+  TestSequence::Unwrapper unwrapper;
+
+  TestSequence s0 = unwrapper.Unwrap(0xFFFE);
+  TestSequence s1 = unwrapper.Unwrap(0xFFFF);
+  TestSequence s2 = unwrapper.Unwrap(0x0000);
+  TestSequence s3 = unwrapper.Unwrap(0x0001);
+
+  EXPECT_LT(s0, s1);
+  EXPECT_LT(s0, s2);
+  EXPECT_LT(s0, s3);
+  EXPECT_LT(s1, s2);
+  EXPECT_LT(s1, s3);
+  EXPECT_LT(s2, s3);
+
+  EXPECT_EQ(s1.Difference(s0), 1);
+  EXPECT_EQ(s2.Difference(s0), 2);
+  EXPECT_EQ(s3.Difference(s0), 3);
+
+  EXPECT_GT(s1, s0);
+  EXPECT_GT(s2, s0);
+  EXPECT_GT(s3, s0);
+  EXPECT_GT(s2, s1);
+  EXPECT_GT(s3, s1);
+  EXPECT_GT(s3, s2);
+
+  s0.Increment();
+  EXPECT_EQ(s0, s1);
+  s1.Increment();
+  EXPECT_EQ(s1, s2);
+  s2.Increment();
+  EXPECT_EQ(s2, s3);
+
+  EXPECT_EQ(s0.AddTo(2), s3);
+}
+
+TEST(SequenceNumbersTest, WrapAroundAFewTimes) {
+  TestSequence::Unwrapper unwrapper;
+
+  TestSequence s0 = unwrapper.Unwrap(0);
+  TestSequence prev = s0;
+
+  for (uint32_t i = 1; i < 65536 * 3; i++) {
+    uint16_t wrapped = static_cast<uint16_t>(i);
+    TestSequence si = unwrapper.Unwrap(wrapped);
+
+    EXPECT_LT(s0, si);
+    EXPECT_LT(prev, si);
+    prev = si;
+  }
+}
+
+TEST(SequenceNumbersTest, IncrementIsSameAsWrapped) {
+  TestSequence::Unwrapper unwrapper;
+
+  TestSequence s0 = unwrapper.Unwrap(0);
+
+  for (uint32_t i = 1; i < 65536 * 2; i++) {
+    uint16_t wrapped = static_cast<uint16_t>(i);
+    TestSequence si = unwrapper.Unwrap(wrapped);
+
+    s0.Increment();
+    EXPECT_EQ(s0, si);
+  }
+}
+
+TEST(SequenceNumbersTest, UnwrappingLargerNumberIsAlwaysLarger) {
+  TestSequence::Unwrapper unwrapper;
+
+  for (uint32_t i = 1; i < 65536 * 2; i++) {
+    uint16_t wrapped = static_cast<uint16_t>(i);
+    TestSequence si = unwrapper.Unwrap(wrapped);
+
+    EXPECT_GT(unwrapper.Unwrap(wrapped + 1), si);
+    EXPECT_GT(unwrapper.Unwrap(wrapped + 5), si);
+    EXPECT_GT(unwrapper.Unwrap(wrapped + 10), si);
+    EXPECT_GT(unwrapper.Unwrap(wrapped + 100), si);
+  }
+}
+
+TEST(SequenceNumbersTest, UnwrappingSmallerNumberIsAlwaysSmaller) {
+  TestSequence::Unwrapper unwrapper;
+
+  for (uint32_t i = 1; i < 65536 * 2; i++) {
+    uint16_t wrapped = static_cast<uint16_t>(i);
+    TestSequence si = unwrapper.Unwrap(wrapped);
+
+    EXPECT_LT(unwrapper.Unwrap(wrapped - 1), si);
+    EXPECT_LT(unwrapper.Unwrap(wrapped - 5), si);
+    EXPECT_LT(unwrapper.Unwrap(wrapped - 10), si);
+    EXPECT_LT(unwrapper.Unwrap(wrapped - 100), si);
+  }
+}
+
+}  // namespace
+}  // namespace dcsctp
diff --git a/net/dcsctp/common/str_join.h b/net/dcsctp/common/str_join.h
new file mode 100644
index 0000000..0451782
--- /dev/null
+++ b/net/dcsctp/common/str_join.h
@@ -0,0 +1,56 @@
+/*
+ *  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_COMMON_STR_JOIN_H_
+#define NET_DCSCTP_COMMON_STR_JOIN_H_
+
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "rtc_base/strings/string_builder.h"
+
+namespace dcsctp {
+
+template <typename Range>
+std::string StrJoin(const Range& seq, absl::string_view delimiter) {
+  rtc::StringBuilder sb;
+  int idx = 0;
+
+  for (const typename Range::value_type& elem : seq) {
+    if (idx > 0) {
+      sb << delimiter;
+    }
+    sb << elem;
+
+    ++idx;
+  }
+  return sb.Release();
+}
+
+template <typename Range, typename Functor>
+std::string StrJoin(const Range& seq,
+                    absl::string_view delimiter,
+                    const Functor& fn) {
+  rtc::StringBuilder sb;
+  int idx = 0;
+
+  for (const typename Range::value_type& elem : seq) {
+    if (idx > 0) {
+      sb << delimiter;
+    }
+    fn(sb, elem);
+
+    ++idx;
+  }
+  return sb.Release();
+}
+
+}  // namespace dcsctp
+
+#endif  // NET_DCSCTP_COMMON_STR_JOIN_H_
diff --git a/net/dcsctp/common/str_join_test.cc b/net/dcsctp/common/str_join_test.cc
new file mode 100644
index 0000000..dbfd92c
--- /dev/null
+++ b/net/dcsctp/common/str_join_test.cc
@@ -0,0 +1,45 @@
+/*
+ *  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/common/str_join.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "test/gmock.h"
+
+namespace dcsctp {
+namespace {
+
+TEST(StrJoinTest, CanJoinStringsFromVector) {
+  std::vector<std::string> strings = {"Hello", "World"};
+  std::string s = StrJoin(strings, " ");
+  EXPECT_EQ(s, "Hello World");
+}
+
+TEST(StrJoinTest, CanJoinNumbersFromArray) {
+  std::array<int, 3> numbers = {1, 2, 3};
+  std::string s = StrJoin(numbers, ",");
+  EXPECT_EQ(s, "1,2,3");
+}
+
+TEST(StrJoinTest, CanFormatElementsWhileJoining) {
+  std::vector<std::pair<std::string, std::string>> pairs = {
+      {"hello", "world"}, {"foo", "bar"}, {"fum", "gazonk"}};
+  std::string s = StrJoin(pairs, ",",
+                          [&](rtc::StringBuilder& sb,
+                              const std::pair<std::string, std::string>& p) {
+                            sb << p.first << "=" << p.second;
+                          });
+  EXPECT_EQ(s, "hello=world,foo=bar,fum=gazonk");
+}
+
+}  // namespace
+}  // namespace dcsctp