Move candidate-attribute logic to the Candidate class

Introduce a new method, `Candidate::ToCandidateAttribute`, to handle
the serialization of a Candidate object into the SDP
'candidate-attribute' string format defined in RFC 5245.

This change is the first step in breaking a circular dependency
between the 'api' target (which contains `Candidate`) and the
'webrtc_sdp' target. Previously, 'api' relied on serialization
functions within 'webrtc_sdp', which in turn depended on 'api'.
By moving this logic into the `Candidate` class itself, 'api' no
longer needs 'webrtc_sdp' for this functionality.

This CL copies the existing logic from `pc/webrtc_sdp.cc` into
`api/candidate.cc` and adds `RTC_DCHECK` assertions to verify that
the new method produces output identical to the old logic. The
old code paths will be removed in a subsequent change.

Bug: webrtc:12330
Change-Id: I5a950e2d131c0ee1400091e454823f67070f730e
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/411040
Commit-Queue: Tomas Gunnarsson <tommi@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#45768}
diff --git a/api/BUILD.gn b/api/BUILD.gn
index 207c169..229e279 100644
--- a/api/BUILD.gn
+++ b/api/BUILD.gn
@@ -208,6 +208,7 @@
     "../rtc_base:crypto_random",
     "../rtc_base:ip_address",
     "../rtc_base:logging",
+    "../rtc_base:net_helper",
     "../rtc_base:network_constants",
     "../rtc_base:socket_address",
     "../rtc_base:stringutils",
diff --git a/api/candidate.cc b/api/candidate.cc
index 1222b67..87e153c 100644
--- a/api/candidate.cc
+++ b/api/candidate.cc
@@ -21,6 +21,7 @@
 #include "rtc_base/crc32.h"
 #include "rtc_base/crypto_random.h"
 #include "rtc_base/ip_address.h"
+#include "rtc_base/net_helper.h"
 #include "rtc_base/network_constants.h"
 #include "rtc_base/socket_address.h"
 #include "rtc_base/strings/string_builder.h"
@@ -28,6 +29,68 @@
 using webrtc::IceCandidateType;
 
 namespace webrtc {
+namespace {
+constexpr char kAttributeCandidate[] = "candidate";
+constexpr char kAttributeCandidateTyp[] = "typ";
+constexpr char kAttributeCandidateRaddr[] = "raddr";
+constexpr char kAttributeCandidateRport[] = "rport";
+constexpr char kAttributeCandidateUfrag[] = "ufrag";
+constexpr char kAttributeCandidateGeneration[] = "generation";
+constexpr char kAttributeCandidateNetworkId[] = "network-id";
+constexpr char kAttributeCandidateNetworkCost[] = "network-cost";
+constexpr absl::string_view kSdpDelimiterColon = ":";
+constexpr char kTcpCandidateType[] = "tcptype";
+
+// Returns the `candidate-attribute` as described in:
+// https://www.rfc-editor.org/rfc/rfc5245#section-15.1
+std::string BuildCandidate(const Candidate& candidate, bool include_ufrag) {
+  StringBuilder os;
+  os << kAttributeCandidate;
+
+  absl::string_view type = candidate.type_name();
+  os << kSdpDelimiterColon << candidate.foundation() << " "
+     << candidate.component() << " " << candidate.protocol() << " "
+     << candidate.priority() << " "
+     << (candidate.address().ipaddr().IsNil()
+             ? candidate.address().hostname()
+             : candidate.address().ipaddr().ToString())
+     << " " << candidate.address().PortAsString() << " "
+     << kAttributeCandidateTyp << " " << type << " ";
+
+  // Related address
+  if (!candidate.related_address().IsNil()) {
+    os << kAttributeCandidateRaddr << " "
+       << candidate.related_address().ipaddr().ToString() << " "
+       << kAttributeCandidateRport << " "
+       << candidate.related_address().PortAsString() << " ";
+  }
+
+  // Note that we allow the tcptype to be missing, for backwards
+  // compatibility; the implementation treats this as a passive candidate.
+  // TODO(bugs.webrtc.org/11466): Treat a missing tcptype as an error?
+  if (candidate.protocol() == TCP_PROTOCOL_NAME &&
+      !candidate.tcptype().empty()) {
+    os << kTcpCandidateType << " " << candidate.tcptype() << " ";
+  }
+
+  // Extensions
+  os << kAttributeCandidateGeneration << " " << candidate.generation();
+  if (include_ufrag && !candidate.username().empty()) {
+    os << " " << kAttributeCandidateUfrag << " " << candidate.username();
+  }
+  if (candidate.network_id() > 0) {
+    os << " " << kAttributeCandidateNetworkId << " " << candidate.network_id();
+  }
+  if (candidate.network_cost() > 0) {
+    os << " " << kAttributeCandidateNetworkCost << " "
+       << candidate.network_cost();
+  }
+
+  return os.str();
+}
+
+}  // namespace
+
 absl::string_view IceCandidateTypeToString(IceCandidateType type) {
   switch (type) {
     case IceCandidateType::kHost:
@@ -40,9 +103,6 @@
       return "relay";
   }
 }
-}  // namespace webrtc
-
-namespace webrtc {
 
 Candidate::Candidate()
     : id_(CreateRandomString(8)),
@@ -135,6 +195,10 @@
   return ost.Release();
 }
 
+std::string Candidate::ToCandidateAttribute(bool include_ufrag) const {
+  return BuildCandidate(*this, include_ufrag);
+}
+
 uint32_t Candidate::GetPriority(uint32_t type_preference,
                                 int network_adapter_preference,
                                 int relay_preference,
diff --git a/api/candidate.h b/api/candidate.h
index 9823107..4b18cac 100644
--- a/api/candidate.h
+++ b/api/candidate.h
@@ -206,6 +206,14 @@
 
   std::string ToSensitiveString() const { return ToStringInternal(true); }
 
+  // Returns the `candidate-attribute` as described in:
+  // https://www.rfc-editor.org/rfc/rfc5245#section-15.1
+  // The returned string will start with "candidate:", not "a=candidate:" and
+  // will not end with "\r\n".
+  // include_ufrag: Controls whether or not the username is included in the
+  // returned string.
+  std::string ToCandidateAttribute(bool include_ufrag) const;
+
   uint32_t GetPriority(uint32_t type_preference,
                        int network_adapter_preference,
                        int relay_preference,
diff --git a/api/candidate_unittest.cc b/api/candidate_unittest.cc
index 2eb9707..ae9ff7e 100644
--- a/api/candidate_unittest.cc
+++ b/api/candidate_unittest.cc
@@ -10,15 +10,27 @@
 
 #include "api/candidate.h"
 
+#include <cstdint>
 #include <string>
 
+#include "absl/strings/string_view.h"
 #include "p2p/base/p2p_constants.h"
 #include "rtc_base/socket_address.h"
 #include "test/gtest.h"
 
-using webrtc::IceCandidateType;
-
 namespace webrtc {
+namespace {
+constexpr absl::string_view kRawCandidate =
+    "candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host generation 2";
+constexpr absl::string_view kRawHostnameCandidate =
+    "candidate:a0+B/1 1 udp 2130706432 a.test 1234 typ host generation 2";
+constexpr char kSdpTcpActiveCandidate[] =
+    "candidate:a0+B/1 1 tcp 2130706432 192.168.1.5 9 typ host "
+    "tcptype active generation 2";
+constexpr uint32_t kCandidatePriority = 2130706432U;  // pref = 1.0
+constexpr uint32_t kCandidateGeneration = 2;
+constexpr char kCandidateFoundation1[] = "a0+B/1";
+}  // namespace
 
 TEST(CandidateTest, Id) {
   Candidate c;
@@ -100,4 +112,50 @@
   EXPECT_NE(foundation1, c.foundation());
 }
 
+TEST(CandidateTest, ToCandidateAttribute) {
+  SocketAddress address("192.168.1.5", 1234);
+  Candidate candidate(ICE_CANDIDATE_COMPONENT_RTP, "udp", address,
+                      kCandidatePriority, "", "", IceCandidateType::kHost,
+                      kCandidateGeneration, kCandidateFoundation1);
+
+  EXPECT_EQ(candidate.ToCandidateAttribute(true), kRawCandidate);
+
+  Candidate candidate_with_ufrag(candidate);
+  candidate_with_ufrag.set_username("ABC");
+  EXPECT_EQ(candidate_with_ufrag.ToCandidateAttribute(true),
+            std::string(kRawCandidate) + " ufrag ABC");
+  EXPECT_EQ(candidate_with_ufrag.ToCandidateAttribute(false), kRawCandidate);
+
+  Candidate candidate_with_network_info(candidate);
+  candidate_with_network_info.set_network_id(1);
+  EXPECT_EQ(candidate_with_network_info.ToCandidateAttribute(true),
+            std::string(kRawCandidate) + " network-id 1");
+  candidate_with_network_info.set_network_cost(999);
+  EXPECT_EQ(candidate_with_network_info.ToCandidateAttribute(true),
+            std::string(kRawCandidate) + " network-id 1 network-cost 999");
+}
+
+TEST(CandidateTest, ToCandidateAttributeHostnameCandidate) {
+  SocketAddress address("a.test", 1234);
+  Candidate candidate(ICE_CANDIDATE_COMPONENT_RTP, "udp", address,
+                      kCandidatePriority, "", "", IceCandidateType::kHost,
+                      kCandidateGeneration, kCandidateFoundation1);
+  EXPECT_EQ(candidate.ToCandidateAttribute(true), kRawHostnameCandidate);
+}
+
+TEST(CandidateTest, ToCandidateAttributeTcpCandidates) {
+  // TODO: webrtc:12330 - This constant is currently defined in port.h
+  // as `TCPTYPE_ACTIVE_STR`. Remove this test only constant and use the
+  // main definition instead when the circular dependency problem has been
+  // fixed.
+  constexpr char TCPTYPE_ACTIVE_STR[] = "active";
+
+  Candidate candidate(ICE_CANDIDATE_COMPONENT_RTP, "tcp",
+                      SocketAddress("192.168.1.5", 9), kCandidatePriority, "",
+                      "", IceCandidateType::kHost, kCandidateGeneration,
+                      kCandidateFoundation1);
+  candidate.set_tcptype(TCPTYPE_ACTIVE_STR);
+  EXPECT_EQ(candidate.ToCandidateAttribute(true), kSdpTcpActiveCandidate);
+}
+
 }  // namespace webrtc
diff --git a/pc/webrtc_sdp.cc b/pc/webrtc_sdp.cc
index 4346364..51c7a6a 100644
--- a/pc/webrtc_sdp.cc
+++ b/pc/webrtc_sdp.cc
@@ -25,6 +25,7 @@
 #include <vector>
 
 #include "absl/algorithm/container.h"
+#include "absl/base/nullability.h"
 #include "absl/strings/ascii.h"
 #include "absl/strings/match.h"
 #include "absl/strings/str_cat.h"
@@ -997,7 +998,16 @@
          << candidate.network_cost();
     }
 
-    AddLine(os.str(), message);
+    const std::string& line = os.str();
+#if RTC_DCHECK_IS_ON
+    // TODO: webrtc:12330 - While moving this functionality into the Candidate
+    // class itself, verify that what this loop produces, is identical to what
+    // the SdpSerialize() method returns.
+    std::string compare = candidate.ToCandidateAttribute(include_ufrag);
+    // Note that SdpSerialize will not return the "a=" part.
+    RTC_DCHECK_EQ(line.substr(2), compare);
+#endif
+    AddLine(line, message);
   }
 }
 
@@ -3339,6 +3349,7 @@
   message.erase(0, 2);
   RTC_DCHECK(message.find(kLineBreak) == message.size() - 2);
   message.resize(message.size() - 2);
+  RTC_DCHECK_EQ(candidate.ToCandidateAttribute(true), message);
   return message;
 }