diff --git a/src/SIPSorcery.csproj b/src/SIPSorcery.csproj index 4fb7ad2eaa..9c09dee2c7 100755 --- a/src/SIPSorcery.csproj +++ b/src/SIPSorcery.csproj @@ -21,6 +21,7 @@ + @@ -35,15 +36,22 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0 12.0 true - $(NoWarn);SYSLIB0050 + $(NoWarn);SYSLIB0050;CS1591;CS1573;CS1587 True + $(WarningsNotAsErrors);NU1510;CS0809;CS0618;CS8632 true - $(NoWarn);CS1591;CS1573;CS1587 Aaron Clauson, Christophe Irles, Rafael Soares & Contributors Copyright © 2010-2025 Aaron Clauson BSD-3-Clause @@ -94,6 +102,7 @@ true snupkg true + true diff --git a/src/core/SIP/SIPAuthorisationDigest.cs b/src/core/SIP/SIPAuthorisationDigest.cs index 02c9cb3f79..6cd760757e 100644 --- a/src/core/SIP/SIPAuthorisationDigest.cs +++ b/src/core/SIP/SIPAuthorisationDigest.cs @@ -256,24 +256,116 @@ public string GetDigest() public override string ToString() { - string authHeader = AuthHeaders.AUTH_DIGEST_KEY + " "; - - authHeader += (Username != null && Username.Trim().Length != 0) ? AuthHeaders.AUTH_USERNAME_KEY + "=\"" + Username + "\"" : null; - authHeader += (authHeader.IndexOf('=') != -1) ? "," + AuthHeaders.AUTH_REALM_KEY + "=\"" + Realm + "\"" : AuthHeaders.AUTH_REALM_KEY + "=\"" + Realm + "\""; - authHeader += (Nonce != null) ? "," + AuthHeaders.AUTH_NONCE_KEY + "=\"" + Nonce + "\"" : null; - authHeader += (URI != null && URI.Trim().Length != 0) ? "," + AuthHeaders.AUTH_URI_KEY + "=\"" + URI + "\"" : null; - authHeader += (Response != null && Response.Length != 0) ? "," + AuthHeaders.AUTH_RESPONSE_KEY + "=\"" + Response + "\"" : null; - authHeader += (Cnonce != null) ? "," + AuthHeaders.AUTH_CNONCE_KEY + "=\"" + Cnonce + "\"" : null; - authHeader += (NonceCount != 0) ? "," + AuthHeaders.AUTH_NONCECOUNT_KEY + "=" + GetPaddedNonceCount(NonceCount) : null; - authHeader += (Qop != null) ? "," + AuthHeaders.AUTH_QOP_KEY + "=" + Qop : null; - authHeader += (Opaque != null) ? "," + AuthHeaders.AUTH_OPAQUE_KEY + "=\"" + Opaque + "\"" : null; - - string algorithmID = (DigestAlgorithm == DigestAlgorithmsEnum.SHA256) ? SHA256_ALGORITHM_ID : DigestAlgorithm.ToString(); - authHeader += (Response != null) ? "," + AuthHeaders.AUTH_ALGORITHM_KEY + "=" + algorithmID : null; - - return authHeader; + var builder = new ValueStringBuilder(); + + try + { + ToString(ref builder); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) + { + builder.Append(AuthHeaders.AUTH_DIGEST_KEY); + builder.Append(' '); + + bool hasUsername = !string.IsNullOrWhiteSpace(Username); + if (hasUsername) + { + builder.Append(AuthHeaders.AUTH_USERNAME_KEY); + builder.Append("=\""); + builder.Append(Username); + builder.Append('"'); + } + + builder.Append(hasUsername ? ',' : '\0'); + builder.Append(AuthHeaders.AUTH_REALM_KEY); + builder.Append("=\""); + builder.Append(Realm); + builder.Append('"'); + + if (Nonce != null) + { + builder.Append(','); + builder.Append(AuthHeaders.AUTH_NONCE_KEY); + builder.Append("=\""); + builder.Append(Nonce); + builder.Append('"'); + } + + if (!string.IsNullOrWhiteSpace(URI)) + { + builder.Append(','); + builder.Append(AuthHeaders.AUTH_URI_KEY); + builder.Append("=\""); + builder.Append(URI); + builder.Append('"'); + } + + if (!string.IsNullOrEmpty(Response)) + { + builder.Append(','); + builder.Append(AuthHeaders.AUTH_RESPONSE_KEY); + builder.Append("=\""); + builder.Append(Response); + builder.Append('"'); + } + + if (Cnonce != null) + { + builder.Append(','); + builder.Append(AuthHeaders.AUTH_CNONCE_KEY); + builder.Append("=\""); + builder.Append(Cnonce); + builder.Append('"'); + } + + if (NonceCount != 0) + { + builder.Append(','); + builder.Append(AuthHeaders.AUTH_NONCECOUNT_KEY); + builder.Append('='); + builder.Append(GetPaddedNonceCount(NonceCount)); + } + + if (Qop != null) + { + builder.Append(','); + builder.Append(AuthHeaders.AUTH_QOP_KEY); + builder.Append('='); + builder.Append(Qop); + } + + if (Opaque != null) + { + builder.Append(','); + builder.Append(AuthHeaders.AUTH_OPAQUE_KEY); + builder.Append("=\""); + builder.Append(Opaque); + builder.Append('"'); + } + + if (Response != null) + { + builder.Append(','); + builder.Append(AuthHeaders.AUTH_ALGORITHM_KEY); + builder.Append('='); + + string algorithmID = (DigestAlgorithm == DigestAlgorithmsEnum.SHA256) + ? SHA256_ALGORITHM_ID + : DigestAlgorithm.ToString(); + + builder.Append(algorithmID); + } } + public SIPAuthorisationDigest CopyOf() { var copy = new SIPAuthorisationDigest(AuthorisationType, Realm, Username, Password, URI, Nonce, RequestType, DigestAlgorithm); @@ -385,7 +477,7 @@ public static string GetHashHex(DigestAlgorithmsEnum hashAlg, string val) case DigestAlgorithmsEnum.SHA256: using (var hash = new SHA256CryptoServiceProvider()) { - return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr().ToLower(); + return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).AsSpan().HexStr(lowercase: true); } // This is commented because RFC8760 does not have an SHA-512 option. Instead it's HSA-512-sess which // means the SIP request body needs to be included in the digest as well. Including the body will require @@ -393,13 +485,13 @@ public static string GetHashHex(DigestAlgorithmsEnum hashAlg, string val) //case DigestAlgorithmsEnum.SHA512: // using (var hash = new SHA512CryptoServiceProvider()) // { - // return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr().ToLower(); + // return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr(lowercase: false); // } case DigestAlgorithmsEnum.MD5: default: using (var hash = new MD5CryptoServiceProvider()) { - return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr().ToLower(); + return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).AsSpan().HexStr(lowercase: true); } } #pragma warning restore SYSLIB0021 diff --git a/src/core/SIP/SIPConstants.cs b/src/core/SIP/SIPConstants.cs index 32da80841d..5caa269c54 100644 --- a/src/core/SIP/SIPConstants.cs +++ b/src/core/SIP/SIPConstants.cs @@ -541,17 +541,68 @@ public static string SIPURIUserUnescape(string escapedString) public static string SIPURIParameterEscape(string unescapedString) { - string result = unescapedString; - if (!result.IsNullOrBlank()) + // Characters that need escaping + ReadOnlySpan specialChars = stackalloc char[] { ';', '?', '@', '=', ',', ' ' }; + + // Early exit if no special characters are found + var unescapedSpan = unescapedString.AsSpan(); + int nextIndex = unescapedSpan.IndexOfAny(specialChars); + if (nextIndex == -1) { - result = result.Replace(";", "%3B"); - result = result.Replace("?", "%3F"); - result = result.Replace("@", "%40"); - result = result.Replace("=", "%3D"); - result = result.Replace(",", "%2C"); - result = result.Replace(" ", "%20"); + // No escaping needed + return unescapedString; + } + + var builder = new ValueStringBuilder(); + + try + { + SIPURIParameterEscape(ref builder, unescapedSpan); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal static void SIPURIParameterEscape(ref ValueStringBuilder builder, ReadOnlySpan unescapedSpan) + { + // Characters that need escaping + ReadOnlySpan specialChars = stackalloc char[] { ';', '?', '@', '=', ',', ' ' }; + + var currentIndex = 0; + var nextIndex = unescapedSpan.IndexOfAny(specialChars); + while (nextIndex != -1) + { + // Append everything before the special character + builder.Append(unescapedSpan.Slice(currentIndex, nextIndex - currentIndex)); + + // Escape the special character + switch (unescapedSpan[nextIndex]) + { + case ';': builder.Append("%3B"); break; + case '?': builder.Append("%3F"); break; + case '@': builder.Append("%40"); break; + case '=': builder.Append("%3D"); break; + case ',': builder.Append("%2C"); break; + case ' ': builder.Append("%20"); break; + } + + currentIndex = nextIndex + 1; + nextIndex = unescapedSpan.Slice(currentIndex).IndexOfAny(specialChars); + if (nextIndex != -1) + { + nextIndex += currentIndex; // Adjust relative index to absolute + } + } + + // Append the remaining part + if (currentIndex < unescapedSpan.Length) + { + builder.Append(unescapedSpan.Slice(currentIndex)); } - return result; } public static string SIPURIParameterUnescape(string escapedString) diff --git a/src/core/SIP/SIPHeader.cs b/src/core/SIP/SIPHeader.cs index e46e041006..1eb72d4cc5 100644 --- a/src/core/SIP/SIPHeader.cs +++ b/src/core/SIP/SIPHeader.cs @@ -18,7 +18,6 @@ using System.Net; using System.Runtime.CompilerServices; using System.Runtime.Serialization; -using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; @@ -355,10 +354,32 @@ public static SIPViaHeader[] ParseSIPViaHeader(string viaHeaderStr) public new string ToString() { - string sipViaHeader = SIPHeaders.SIP_HEADER_VIA + ": " + this.Version + "/" + this.Transport.ToString().ToUpper() + " " + ContactAddress; - sipViaHeader += (ViaParameters != null && ViaParameters.Count > 0) ? ViaParameters.ToString() : null; + var builder = new ValueStringBuilder(); + try + { + ToString(ref builder); + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) + { + builder.Append(SIPHeaders.SIP_HEADER_VIA); + builder.Append(": "); + builder.Append(Version); + builder.Append('/'); + builder.Append(Transport.ToString().ToUpperInvariant()); + builder.Append(' '); + builder.Append(ContactAddress); - return sipViaHeader; + if (ViaParameters != null && ViaParameters.Count > 0) + { + builder.Append(ViaParameters.ToString()); + } } } @@ -466,7 +487,23 @@ public static SIPFromHeader ParseFromHeader(string fromHeaderStr) public override string ToString() { - return m_userField.ToString(); + var builder = new ValueStringBuilder(); + + try + { + ToString(ref builder); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) + { + m_userField.ToString(ref builder); } /// @@ -574,7 +611,23 @@ public static SIPToHeader ParseToHeader(string toHeaderStr) public override string ToString() { - return m_userField.ToString(); + var builder = new ValueStringBuilder(); + + try + { + ToString(ref builder); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) + { + m_userField.ToString(ref builder); } } @@ -797,16 +850,30 @@ public override string ToString() { return SIPConstants.SIP_REGISTER_REMOVEALL; } + + var builder = new ValueStringBuilder(); + + try + { + ToString(ref builder); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) + { + if (m_userField.URI.Host == SIPConstants.SIP_REGISTER_REMOVEALL) + { + builder.Append(SIPConstants.SIP_REGISTER_REMOVEALL); + } else { - //if (m_userField.URI.Protocol == SIPProtocolsEnum.UDP) - //{ - return m_userField.ToString(); - //} - //else - //{ - // return m_userField.ToContactString(); - //} + m_userField.ToString(ref builder); } } @@ -902,21 +969,38 @@ private static string BuildAuthorisationHeaderName(SIPAuthorisationHeadersEnum a } public override string ToString() + { + var builder = new ValueStringBuilder(); + + try + { + ToString(ref builder); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) { if (SIPDigest != null) { - var authorisationHeaderType = (SIPDigest.AuthorisationResponseType != SIPAuthorisationHeadersEnum.Unknown) ? SIPDigest.AuthorisationResponseType : SIPDigest.AuthorisationType; + var authorisationHeaderType = (SIPDigest.AuthorisationResponseType != SIPAuthorisationHeadersEnum.Unknown) + ? SIPDigest.AuthorisationResponseType + : SIPDigest.AuthorisationType; + string authHeader = BuildAuthorisationHeaderName(authorisationHeaderType); - return authHeader + SIPDigest.ToString(); + builder.Append(authHeader); + SIPDigest.ToString(ref builder); } else if (!string.IsNullOrEmpty(Value)) { string authHeader = BuildAuthorisationHeaderName(AuthorisationType); - return authHeader + Value; - } - else - { - return null; + builder.Append(authHeader); + builder.Append(Value); } } } @@ -1153,7 +1237,7 @@ public void RemoveBottomRoute() if (m_sipRoutes.Count > 0) { m_sipRoutes.RemoveAt(m_sipRoutes.Count - 1); - }; + } } public SIPRouteSet Reversed() @@ -1191,19 +1275,36 @@ public void ReplaceRoute(string origSocket, string replacementSocket) } } - public new string ToString() + public override string ToString() + { + var builder = new ValueStringBuilder(); + + try + { + ToString(ref builder); + + return builder.ToString(); + } + finally { - string routeStr = null; + builder.Dispose(); + } + } + internal void ToString(ref ValueStringBuilder builder) + { if (m_sipRoutes != null && m_sipRoutes.Count > 0) { for (int routeIndex = 0; routeIndex < m_sipRoutes.Count; routeIndex++) { - routeStr += (routeStr != null) ? "," + m_sipRoutes[routeIndex].ToString() : m_sipRoutes[routeIndex].ToString(); + if (routeIndex > 0) + { + builder.Append(","); + } + + builder.Append(m_sipRoutes[routeIndex].ToString()); } } - - return routeStr; } } @@ -1307,17 +1408,30 @@ public void PushViaHeader(SIPViaHeader viaHeader) public new string ToString() { - string viaStr = null; + var builder = new ValueStringBuilder(); + + try + { + ToString(ref builder); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + internal void ToString(ref ValueStringBuilder builder) + { if (m_viaHeaders != null && m_viaHeaders.Count > 0) { for (int viaIndex = 0; viaIndex < m_viaHeaders.Count; viaIndex++) { - viaStr += (m_viaHeaders[viaIndex]).ToString() + m_CRLF; + m_viaHeaders[viaIndex].ToString(ref builder); + builder.Append(m_CRLF); } } - - return viaStr; } } @@ -2201,148 +2315,531 @@ public static SIPHeader ParseSIPHeaders(string[] headersCollection) /// String representing the SIP headers. public new string ToString() { + var builder = new ValueStringBuilder(); + try { - StringBuilder headersBuilder = new StringBuilder(); + ToString(ref builder); + + return builder.ToString(); + } + catch (Exception excp) + { + logger.LogError(excp, "Exception SIPHeader ToString. Exception: {ErrorMessage}", excp.Message); + throw; + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) + { + Vias.ToString(ref builder); + + if (To != null) + { + builder.Append(SIPHeaders.SIP_HEADER_TO); + builder.Append(": "); + To.ToString(ref builder); + builder.Append(m_CRLF); + } + + if (From != null) + { + builder.Append(SIPHeaders.SIP_HEADER_FROM); + builder.Append(": "); + From.ToString(ref builder); + builder.Append(m_CRLF); + } + + if (CallId != null) + { + builder.Append(SIPHeaders.SIP_HEADER_CALLID); + builder.Append(": "); + builder.Append(CallId); + builder.Append(m_CRLF); + } - headersBuilder.Append(Vias.ToString()); + if (CSeq >= 0) + { + builder.Append(SIPHeaders.SIP_HEADER_CSEQ); + builder.Append(": "); + builder.Append(CSeq); - string cseqField = null; - if (this.CSeq >= 0) + if (CSeqMethod != SIPMethodsEnum.NONE) { - cseqField = (this.CSeqMethod != SIPMethodsEnum.NONE) ? this.CSeq + " " + this.CSeqMethod.ToString() : this.CSeq.ToString(); + builder.Append(' '); + builder.Append(CSeqMethod.ToString()); } - headersBuilder.Append((To != null) ? SIPHeaders.SIP_HEADER_TO + ": " + this.To.ToString() + m_CRLF : null); - headersBuilder.Append((From != null) ? SIPHeaders.SIP_HEADER_FROM + ": " + this.From.ToString() + m_CRLF : null); - headersBuilder.Append((CallId != null) ? SIPHeaders.SIP_HEADER_CALLID + ": " + this.CallId + m_CRLF : null); - headersBuilder.Append((CSeq >= 0) ? SIPHeaders.SIP_HEADER_CSEQ + ": " + cseqField + m_CRLF : null); + builder.Append(m_CRLF); + } #region Appending Contact header. if (Contact != null && Contact.Count == 1) { - headersBuilder.Append(SIPHeaders.SIP_HEADER_CONTACT + ": " + Contact[0].ToString() + m_CRLF); + builder.Append(SIPHeaders.SIP_HEADER_CONTACT); + builder.Append(": "); + Contact[0].ToString(ref builder); + builder.Append(m_CRLF); } else if (Contact != null && Contact.Count > 1) { - StringBuilder contactsBuilder = new StringBuilder(); - contactsBuilder.Append(SIPHeaders.SIP_HEADER_CONTACT + ": "); + builder.Append(SIPHeaders.SIP_HEADER_CONTACT); + builder.Append(": "); bool firstContact = true; foreach (SIPContactHeader contactHeader in Contact) { - if (firstContact) + if (!firstContact) { - contactsBuilder.Append(contactHeader.ToString()); + builder.Append(','); } - else + + contactHeader.ToString(ref builder); + firstContact = false; + } + + builder.Append(m_CRLF); + } + + #endregion + + if (MaxForwards >= 0) + { + builder.Append(SIPHeaders.SIP_HEADER_MAXFORWARDS); + builder.Append(": "); + builder.Append(MaxForwards); + builder.Append(m_CRLF); + } + + if (Routes != null && Routes.Length > 0) + { + builder.Append(SIPHeaders.SIP_HEADER_ROUTE); + builder.Append(": "); + Routes.ToString(ref builder); + builder.Append(m_CRLF); + } + + if (RecordRoutes != null && RecordRoutes.Length > 0) + { + builder.Append(SIPHeaders.SIP_HEADER_RECORDROUTE); + builder.Append(": "); + RecordRoutes.ToString(ref builder); + builder.Append(m_CRLF); + } + + if (UserAgent != null && UserAgent.Trim().Length != 0) + { + builder.Append(SIPHeaders.SIP_HEADER_USERAGENT); + builder.Append(": "); + builder.Append(UserAgent); + builder.Append(m_CRLF); + } + + if (Expires != -1) + { + builder.Append(SIPHeaders.SIP_HEADER_EXPIRES); + builder.Append(": "); + builder.Append(Expires); + builder.Append(m_CRLF); + } + + if (MinExpires != -1) + { + builder.Append(SIPHeaders.SIP_HEADER_MINEXPIRES); + builder.Append(": "); + builder.Append(MinExpires); + builder.Append(m_CRLF); + } + + if (Accept != null) + { + builder.Append(SIPHeaders.SIP_HEADER_ACCEPT); + builder.Append(": "); + builder.Append(Accept); + builder.Append(m_CRLF); + } + + if (AcceptEncoding != null) { - contactsBuilder.Append("," + contactHeader.ToString()); + builder.Append(SIPHeaders.SIP_HEADER_ACCEPTENCODING); + builder.Append(": "); + builder.Append(AcceptEncoding); + builder.Append(m_CRLF); } - firstContact = false; + if (AcceptLanguage != null) + { + builder.Append(SIPHeaders.SIP_HEADER_ACCEPTLANGUAGE); + builder.Append(": "); + builder.Append(AcceptLanguage); + builder.Append(m_CRLF); } - headersBuilder.Append(contactsBuilder.ToString() + m_CRLF); + if (Allow != null) + { + builder.Append(SIPHeaders.SIP_HEADER_ALLOW); + builder.Append(": "); + builder.Append(Allow); + builder.Append(m_CRLF); } - #endregion - - headersBuilder.Append((MaxForwards >= 0) ? SIPHeaders.SIP_HEADER_MAXFORWARDS + ": " + this.MaxForwards + m_CRLF : null); - headersBuilder.Append((Routes != null && Routes.Length > 0) ? SIPHeaders.SIP_HEADER_ROUTE + ": " + Routes.ToString() + m_CRLF : null); - headersBuilder.Append((RecordRoutes != null && RecordRoutes.Length > 0) ? SIPHeaders.SIP_HEADER_RECORDROUTE + ": " + RecordRoutes.ToString() + m_CRLF : null); - headersBuilder.Append((UserAgent != null && UserAgent.Trim().Length != 0) ? SIPHeaders.SIP_HEADER_USERAGENT + ": " + this.UserAgent + m_CRLF : null); - headersBuilder.Append((Expires != -1) ? SIPHeaders.SIP_HEADER_EXPIRES + ": " + this.Expires + m_CRLF : null); - headersBuilder.Append((MinExpires != -1) ? SIPHeaders.SIP_HEADER_MINEXPIRES + ": " + this.MinExpires + m_CRLF : null); - headersBuilder.Append((Accept != null) ? SIPHeaders.SIP_HEADER_ACCEPT + ": " + this.Accept + m_CRLF : null); - headersBuilder.Append((AcceptEncoding != null) ? SIPHeaders.SIP_HEADER_ACCEPTENCODING + ": " + this.AcceptEncoding + m_CRLF : null); - headersBuilder.Append((AcceptLanguage != null) ? SIPHeaders.SIP_HEADER_ACCEPTLANGUAGE + ": " + this.AcceptLanguage + m_CRLF : null); - headersBuilder.Append((Allow != null) ? SIPHeaders.SIP_HEADER_ALLOW + ": " + this.Allow + m_CRLF : null); - headersBuilder.Append((AlertInfo != null) ? SIPHeaders.SIP_HEADER_ALERTINFO + ": " + this.AlertInfo + m_CRLF : null); - headersBuilder.Append((AuthenticationInfo != null) ? SIPHeaders.SIP_HEADER_AUTHENTICATIONINFO + ": " + this.AuthenticationInfo + m_CRLF : null); + if (AlertInfo != null) + { + builder.Append(SIPHeaders.SIP_HEADER_ALERTINFO); + builder.Append(": "); + builder.Append(AlertInfo); + builder.Append(m_CRLF); + } + + if (AuthenticationInfo != null) + { + builder.Append(SIPHeaders.SIP_HEADER_AUTHENTICATIONINFO); + builder.Append(": "); + builder.Append(AuthenticationInfo); + builder.Append(m_CRLF); + } if (AuthenticationHeaders.Count > 0) { foreach (var authHeader in AuthenticationHeaders) { - var value = authHeader.ToString(); - if (value != null) + if (authHeader is not null) { - headersBuilder.Append(authHeader.ToString() + m_CRLF); + authHeader.ToString(ref builder); + builder.Append(m_CRLF); + } + } + } + + if (CallInfo != null) + { + builder.Append(SIPHeaders.SIP_HEADER_CALLINFO); + builder.Append(": "); + builder.Append(CallInfo); + builder.Append(m_CRLF); + } + + if (ContentDisposition != null) + { + builder.Append(SIPHeaders.SIP_HEADER_CONTENT_DISPOSITION); + builder.Append(": "); + builder.Append(ContentDisposition); + builder.Append(m_CRLF); + } + + if (ContentEncoding != null) + { + builder.Append(SIPHeaders.SIP_HEADER_CONTENT_ENCODING); + builder.Append(": "); + builder.Append(ContentEncoding); + builder.Append(m_CRLF); + } + + if (ContentLanguage != null) + { + builder.Append(SIPHeaders.SIP_HEADER_CONTENT_LANGUAGE); + builder.Append(": "); + builder.Append(ContentLanguage); + builder.Append(m_CRLF); + } + + if (Date != null) + { + builder.Append(SIPHeaders.SIP_HEADER_DATE); + builder.Append(": "); + builder.Append(Date); + builder.Append(m_CRLF); + } + + if (ErrorInfo != null) + { + builder.Append(SIPHeaders.SIP_HEADER_ERROR_INFO); + builder.Append(": "); + builder.Append(ErrorInfo); + builder.Append(m_CRLF); + } + + if (InReplyTo != null) + { + builder.Append(SIPHeaders.SIP_HEADER_IN_REPLY_TO); + builder.Append(": "); + builder.Append(InReplyTo); + builder.Append(m_CRLF); + } + + if (Organization != null) + { + builder.Append(SIPHeaders.SIP_HEADER_ORGANIZATION); + builder.Append(": "); + builder.Append(Organization); + builder.Append(m_CRLF); + } + + if (Priority != null) + { + builder.Append(SIPHeaders.SIP_HEADER_PRIORITY); + builder.Append(": "); + builder.Append(Priority); + builder.Append(m_CRLF); } + + if (ProxyRequire != null) + { + builder.Append(SIPHeaders.SIP_HEADER_PROXY_REQUIRE); + builder.Append(": "); + builder.Append(ProxyRequire); + builder.Append(m_CRLF); + } + + if (ReplyTo != null) + { + builder.Append(SIPHeaders.SIP_HEADER_REPLY_TO); + builder.Append(": "); + builder.Append(ReplyTo); + builder.Append(m_CRLF); + } + + if (Require != null) + { + builder.Append(SIPHeaders.SIP_HEADER_REQUIRE); + builder.Append(": "); + builder.Append(Require); + builder.Append(m_CRLF); + } + + if (RetryAfter != null) + { + builder.Append(SIPHeaders.SIP_HEADER_RETRY_AFTER); + builder.Append(": "); + builder.Append(RetryAfter); + builder.Append(m_CRLF); + } + + if (Server != null && Server.Trim().Length != 0) + { + builder.Append(SIPHeaders.SIP_HEADER_SERVER); + builder.Append(": "); + builder.Append(Server); + builder.Append(m_CRLF); + } + + if (Subject != null) + { + builder.Append(SIPHeaders.SIP_HEADER_SUBJECT); + builder.Append(": "); + builder.Append(Subject); + builder.Append(m_CRLF); } + + if (Supported != null) + { + builder.Append(SIPHeaders.SIP_HEADER_SUPPORTED); + builder.Append(": "); + builder.Append(Supported); + builder.Append(m_CRLF); } - headersBuilder.Append((CallInfo != null) ? SIPHeaders.SIP_HEADER_CALLINFO + ": " + this.CallInfo + m_CRLF : null); - headersBuilder.Append((ContentDisposition != null) ? SIPHeaders.SIP_HEADER_CONTENT_DISPOSITION + ": " + this.ContentDisposition + m_CRLF : null); - headersBuilder.Append((ContentEncoding != null) ? SIPHeaders.SIP_HEADER_CONTENT_ENCODING + ": " + this.ContentEncoding + m_CRLF : null); - headersBuilder.Append((ContentLanguage != null) ? SIPHeaders.SIP_HEADER_CONTENT_LANGUAGE + ": " + this.ContentLanguage + m_CRLF : null); - headersBuilder.Append((Date != null) ? SIPHeaders.SIP_HEADER_DATE + ": " + Date + m_CRLF : null); - headersBuilder.Append((ErrorInfo != null) ? SIPHeaders.SIP_HEADER_ERROR_INFO + ": " + this.ErrorInfo + m_CRLF : null); - headersBuilder.Append((InReplyTo != null) ? SIPHeaders.SIP_HEADER_IN_REPLY_TO + ": " + this.InReplyTo + m_CRLF : null); - headersBuilder.Append((Organization != null) ? SIPHeaders.SIP_HEADER_ORGANIZATION + ": " + this.Organization + m_CRLF : null); - headersBuilder.Append((Priority != null) ? SIPHeaders.SIP_HEADER_PRIORITY + ": " + Priority + m_CRLF : null); - headersBuilder.Append((ProxyRequire != null) ? SIPHeaders.SIP_HEADER_PROXY_REQUIRE + ": " + this.ProxyRequire + m_CRLF : null); - headersBuilder.Append((ReplyTo != null) ? SIPHeaders.SIP_HEADER_REPLY_TO + ": " + this.ReplyTo + m_CRLF : null); - headersBuilder.Append((Require != null) ? SIPHeaders.SIP_HEADER_REQUIRE + ": " + Require + m_CRLF : null); - headersBuilder.Append((RetryAfter != null) ? SIPHeaders.SIP_HEADER_RETRY_AFTER + ": " + this.RetryAfter + m_CRLF : null); - headersBuilder.Append((Server != null && Server.Trim().Length != 0) ? SIPHeaders.SIP_HEADER_SERVER + ": " + this.Server + m_CRLF : null); - headersBuilder.Append((Subject != null) ? SIPHeaders.SIP_HEADER_SUBJECT + ": " + Subject + m_CRLF : null); - headersBuilder.Append((Supported != null) ? SIPHeaders.SIP_HEADER_SUPPORTED + ": " + Supported + m_CRLF : null); - headersBuilder.Append((Timestamp != null) ? SIPHeaders.SIP_HEADER_TIMESTAMP + ": " + Timestamp + m_CRLF : null); - headersBuilder.Append((Unsupported != null) ? SIPHeaders.SIP_HEADER_UNSUPPORTED + ": " + Unsupported + m_CRLF : null); - headersBuilder.Append((Warning != null) ? SIPHeaders.SIP_HEADER_WARNING + ": " + Warning + m_CRLF : null); - headersBuilder.Append((ETag != null) ? SIPHeaders.SIP_HEADER_ETAG + ": " + ETag + m_CRLF : null); - headersBuilder.Append(SIPHeaders.SIP_HEADER_CONTENTLENGTH + ": " + this.ContentLength + m_CRLF); - if (this.ContentType != null && this.ContentType.Trim().Length > 0) + + if (Timestamp != null) + { + builder.Append(SIPHeaders.SIP_HEADER_TIMESTAMP); + builder.Append(": "); + builder.Append(Timestamp); + builder.Append(m_CRLF); + } + + if (Unsupported != null) + { + builder.Append(SIPHeaders.SIP_HEADER_UNSUPPORTED); + builder.Append(": "); + builder.Append(Unsupported); + builder.Append(m_CRLF); + } + + if (Warning != null) + { + builder.Append(SIPHeaders.SIP_HEADER_WARNING); + builder.Append(": "); + builder.Append(Warning); + builder.Append(m_CRLF); + } + + if (ETag != null) + { + builder.Append(SIPHeaders.SIP_HEADER_ETAG); + builder.Append(": "); + builder.Append(ETag); + builder.Append(m_CRLF); + } + + builder.Append(SIPHeaders.SIP_HEADER_CONTENTLENGTH); + builder.Append(": "); + builder.Append(ContentLength); + builder.Append(m_CRLF); + + if (ContentType != null && ContentType.Trim().Length > 0) { - headersBuilder.Append(SIPHeaders.SIP_HEADER_CONTENTTYPE + ": " + this.ContentType + m_CRLF); + builder.Append(SIPHeaders.SIP_HEADER_CONTENTTYPE); + builder.Append(": "); + builder.Append(ContentType); + builder.Append(m_CRLF); } // Non-core SIP headers. - headersBuilder.Append((AllowEvents != null) ? SIPHeaders.SIP_HEADER_ALLOW_EVENTS + ": " + AllowEvents + m_CRLF : null); - headersBuilder.Append((Event != null) ? SIPHeaders.SIP_HEADER_EVENT + ": " + Event + m_CRLF : null); - headersBuilder.Append((SubscriptionState != null) ? SIPHeaders.SIP_HEADER_SUBSCRIPTION_STATE + ": " + SubscriptionState + m_CRLF : null); - headersBuilder.Append((ReferSub != null) ? SIPHeaders.SIP_HEADER_REFERSUB + ": " + ReferSub + m_CRLF : null); - headersBuilder.Append((ReferTo != null) ? SIPHeaders.SIP_HEADER_REFERTO + ": " + ReferTo + m_CRLF : null); - headersBuilder.Append((ReferredBy != null) ? SIPHeaders.SIP_HEADER_REFERREDBY + ": " + ReferredBy + m_CRLF : null); - headersBuilder.Append((Replaces != null) ? SIPHeaders.SIP_HEADER_REPLACES + ": " + Replaces + m_CRLF : null); - headersBuilder.Append((Reason != null) ? SIPHeaders.SIP_HEADER_REASON + ": " + Reason + m_CRLF : null); - headersBuilder.Append((RSeq != -1) ? SIPHeaders.SIP_HEADER_RELIABLE_SEQ + ": " + RSeq + m_CRLF : null); - headersBuilder.Append((RAckRSeq != -1) ? SIPHeaders.SIP_HEADER_RELIABLE_ACK + ": " + RAckRSeq + " " + RAckCSeq + " " + RAckCSeqMethod + m_CRLF : null); + if (AllowEvents != null) + { + builder.Append(SIPHeaders.SIP_HEADER_ALLOW_EVENTS); + builder.Append(": "); + builder.Append(AllowEvents); + builder.Append(m_CRLF); + } + + if (Event != null) + { + builder.Append(SIPHeaders.SIP_HEADER_EVENT); + builder.Append(": "); + builder.Append(Event); + builder.Append(m_CRLF); + } + + if (SubscriptionState != null) + { + builder.Append(SIPHeaders.SIP_HEADER_SUBSCRIPTION_STATE); + builder.Append(": "); + builder.Append(SubscriptionState); + builder.Append(m_CRLF); + } + + if (ReferSub != null) + { + builder.Append(SIPHeaders.SIP_HEADER_REFERSUB); + builder.Append(": "); + builder.Append(ReferSub); + builder.Append(m_CRLF); + } + + if (ReferTo != null) + { + builder.Append(SIPHeaders.SIP_HEADER_REFERTO); + builder.Append(": "); + builder.Append(ReferTo); + builder.Append(m_CRLF); + } + + if (ReferredBy != null) + { + builder.Append(SIPHeaders.SIP_HEADER_REFERREDBY); + builder.Append(": "); + builder.Append(ReferredBy); + builder.Append(m_CRLF); + } + + if (Replaces != null) + { + builder.Append(SIPHeaders.SIP_HEADER_REPLACES); + builder.Append(": "); + builder.Append(Replaces); + builder.Append(m_CRLF); + } + + if (Reason != null) + { + builder.Append(SIPHeaders.SIP_HEADER_REASON); + builder.Append(": "); + builder.Append(Reason); + builder.Append(m_CRLF); + } + + if (RSeq != -1) + { + builder.Append(SIPHeaders.SIP_HEADER_RELIABLE_SEQ); + builder.Append(": "); + builder.Append(RSeq); + builder.Append(m_CRLF); + } + + if (RAckRSeq != -1) + { + builder.Append(SIPHeaders.SIP_HEADER_RELIABLE_ACK); + builder.Append(": "); + builder.Append(RAckRSeq); + builder.Append(' '); + builder.Append(RAckCSeq); + builder.Append(' '); + builder.Append(RAckCSeqMethod.ToString()); + builder.Append(m_CRLF); + } foreach (var PAI in PassertedIdentity) { - headersBuilder.Append(SIPHeaders.SIP_HEADER_PASSERTED_IDENTITY + ": " + PAI + m_CRLF); + if (PAI != null) + { + builder.Append(SIPHeaders.SIP_HEADER_PASSERTED_IDENTITY); + builder.Append(": "); + builder.Append(PAI.ToString()); + builder.Append(m_CRLF); + } } foreach (var HistInfo in HistoryInfo) { - headersBuilder.Append(SIPHeaders.SIP_HEADER_HISTORY_INFO + ": " + HistInfo + m_CRLF); + if (HistInfo != null) + { + builder.Append(SIPHeaders.SIP_HEADER_HISTORY_INFO); + builder.Append(": "); + builder.Append(HistInfo.ToString()); + builder.Append(m_CRLF); + } } foreach (var DiversionHeader in Diversion) { - headersBuilder.Append(SIPHeaders.SIP_HEADER_DIVERSION + ": " + DiversionHeader + m_CRLF); + if (DiversionHeader != null) + { + builder.Append(SIPHeaders.SIP_HEADER_DIVERSION); + builder.Append(": "); + builder.Append(DiversionHeader.ToString()); + builder.Append(m_CRLF); + } } // Custom SIP headers. - headersBuilder.Append((ProxyReceivedFrom != null) ? SIPHeaders.SIP_HEADER_PROXY_RECEIVEDFROM + ": " + ProxyReceivedFrom + m_CRLF : null); - headersBuilder.Append((ProxyReceivedOn != null) ? SIPHeaders.SIP_HEADER_PROXY_RECEIVEDON + ": " + ProxyReceivedOn + m_CRLF : null); - headersBuilder.Append((ProxySendFrom != null) ? SIPHeaders.SIP_HEADER_PROXY_SENDFROM + ": " + ProxySendFrom + m_CRLF : null); + if (ProxyReceivedFrom != null) + { + builder.Append(SIPHeaders.SIP_HEADER_PROXY_RECEIVEDFROM); + builder.Append(": "); + builder.Append(ProxyReceivedFrom); + builder.Append(m_CRLF); + } - // Unknown SIP headers - foreach (string unknownHeader in UnknownHeaders) + if (ProxyReceivedOn != null) { - headersBuilder.Append(unknownHeader + m_CRLF); + builder.Append(SIPHeaders.SIP_HEADER_PROXY_RECEIVEDON); + builder.Append(": "); + builder.Append(ProxyReceivedOn); + builder.Append(m_CRLF); } - return headersBuilder.ToString(); + if (ProxySendFrom != null) + { + builder.Append(SIPHeaders.SIP_HEADER_PROXY_SENDFROM); + builder.Append(": "); + builder.Append(ProxySendFrom); + builder.Append(m_CRLF); } - catch (Exception excp) + + // Unknown SIP headers + foreach (string unknownHeader in UnknownHeaders) { - logger.LogError(excp, "Exception SIPHeader ToString. Exception: {ErrorMessage}", excp.Message); - throw; + if (unknownHeader != null) + { + builder.Append(unknownHeader); + builder.Append(m_CRLF); + } } } diff --git a/src/core/SIP/SIPMessageBase.cs b/src/core/SIP/SIPMessageBase.cs index 5bdb00da29..bb8b019f38 100644 --- a/src/core/SIP/SIPMessageBase.cs +++ b/src/core/SIP/SIPMessageBase.cs @@ -1,32 +1,32 @@ -//----------------------------------------------------------------------------- -// Filename: SIPMessageBase.cs -// -// Description: Common base class for SIPRequest and SIPResponse classes. -// -// Author(s): +//----------------------------------------------------------------------------- +// Filename: SIPMessageBase.cs +// +// Description: Common base class for SIPRequest and SIPResponse classes. +// +// Author(s): // Salih YILDIRIM (github: salihy) -// Aaron Clauson (aaron@sipsorcery.com) -// -// History: +// Aaron Clauson (aaron@sipsorcery.com) +// +// History: // 26 Nov 2019 Salih YILDIRIM Created. -// 26 Nov 2019 Aaron Clauson Converted from interface to base class to extract common properties. -// -// License: -// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. -//----------------------------------------------------------------------------- +// 26 Nov 2019 Aaron Clauson Converted from interface to base class to extract common properties. +// +// License: +// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. +//----------------------------------------------------------------------------- using System; using System.Text; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; - -namespace SIPSorcery.SIP -{ - public class SIPMessageBase + +namespace SIPSorcery.SIP +{ + public class SIPMessageBase { protected static ILogger logger = Log.Logger; - protected const string m_CRLF = SIPConstants.CRLF; + protected const string m_CRLF = SIPConstants.CRLF; protected const string m_sipFullVersion = SIPConstants.SIP_FULLVERSION_STRING; protected const string m_allowedSIPMethods = SIPConstants.ALLOWED_SIP_METHODS; @@ -43,15 +43,15 @@ public SIPMessageBase(Encoding sipEncoding, Encoding sipBodyEncoding) SIPBodyEncoding = sipBodyEncoding; } - /// - /// The SIP request/response's headers collection. - /// + /// + /// The SIP request/response's headers collection. + /// public SIPHeader Header; - /// + /// /// The optional body or payload for the SIP request/response. This Body property - /// should be used for Session Description Protocol (SDP) and other string payloads. - /// + /// should be used for Session Description Protocol (SDP) and other string payloads. + /// public string Body { get @@ -87,51 +87,65 @@ public byte[] BodyBuffer { get => _body; set => _body = value; - } - - /// - /// Timestamp for the SIP request/response's creation. - /// - public DateTime Created = DateTime.Now; - - /// - /// The remote SIP socket the request/response was received from. - /// - public SIPEndPoint RemoteSIPEndPoint { get; protected set; } - - /// - /// The local SIP socket the request/response was received on. - /// + } + + /// + /// Timestamp for the SIP request/response's creation. + /// + public DateTime Created = DateTime.Now; + + /// + /// The remote SIP socket the request/response was received from. + /// + public SIPEndPoint RemoteSIPEndPoint { get; protected set; } + + /// + /// The local SIP socket the request/response was received on. + /// public SIPEndPoint LocalSIPEndPoint { get; protected set; } - /// + /// /// When the SIP transport layer has multiple channels it will use this ID hint to choose amongst them when - /// sending this request/response. - /// - public string SendFromHintChannelID; - - /// + /// sending this request/response. + /// + public string SendFromHintChannelID; + + /// /// For connection oriented SIP transport channels this ID provides a hint about the specific connection to use - /// when sending this request/response. - /// + /// when sending this request/response. + /// public string SendFromHintConnectionID; protected byte[] GetBytes(string firstLine) { - string headers = firstLine + this.Header.ToString() + m_CRLF; + var builder = new ValueStringBuilder(); - if (_body != null && _body.Length > 0) + try { - var headerBytes = SIPEncoding.GetBytes(headers); - byte[] buffer = new byte[headerBytes.Length + _body.Length]; - Buffer.BlockCopy(headerBytes, 0, buffer, 0, headerBytes.Length); - Buffer.BlockCopy(_body, 0, buffer, headerBytes.Length, _body.Length); + builder.Append(firstLine); + this.Header.ToString(ref builder); + builder.Append(m_CRLF); + + if (_body is { Length: > 0 }) + { + var headerByteCount = SIPEncoding.GetByteCount(builder.AsSpan()); + var buffer = new byte[headerByteCount + _body.Length]; + SIPEncoding.GetBytes(builder.AsSpan(), buffer.AsSpan()); + _body.CopyTo(buffer.AsSpan(headerByteCount)); return buffer; } else { - return SIPEncoding.GetBytes(headers); + var headerByteCount = SIPEncoding.GetByteCount(builder.AsSpan()); + var buffer = new byte[headerByteCount]; + SIPEncoding.GetBytes(builder.AsSpan(), buffer.AsSpan()); + return buffer; + } + } + finally + { + builder.Dispose(); } } - } -} + } +} diff --git a/src/core/SIP/SIPParameters.cs b/src/core/SIP/SIPParameters.cs index ab5f45aa93..f1951a57df 100644 --- a/src/core/SIP/SIPParameters.cs +++ b/src/core/SIP/SIPParameters.cs @@ -289,24 +289,51 @@ public string[] GetKeys() public override string ToString() { - string paramStr = null; + var builder = new ValueStringBuilder(); + try + { + ToString(ref builder, TagDelimiter); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder, char firstDelimiter) + { if (m_dictionary != null) { - foreach (KeyValuePair param in m_dictionary) + bool isFirst = true; + + foreach (var (key, value) in m_dictionary) { - if (param.Value != null && param.Value.Trim().Length > 0) + if (isFirst) { - paramStr += TagDelimiter + param.Key + TAG_NAME_VALUE_SEPERATOR + SIPEscape.SIPURIParameterEscape(param.Value); + builder.Append(firstDelimiter); + isFirst = false; } else { - paramStr += TagDelimiter + param.Key; + builder.Append(TagDelimiter); + } + + builder.Append(key); + + if (value is not null) + { + var valueSpan = value.AsSpan(); + if (!valueSpan.Trim().IsEmpty) + { + builder.Append(TAG_NAME_VALUE_SEPERATOR); + SIPEscape.SIPURIParameterEscape(ref builder, valueSpan); + } } } } - - return paramStr; } public override int GetHashCode() diff --git a/src/core/SIP/SIPURI.cs b/src/core/SIP/SIPURI.cs index e7cff1ae70..d44c960044 100644 --- a/src/core/SIP/SIPURI.cs +++ b/src/core/SIP/SIPURI.cs @@ -485,35 +485,57 @@ public static bool TryParse(string uriStr, out SIPURI uri) public override string ToString() { + var builder = new ValueStringBuilder(); + try { - string uriStr = Scheme.ToString() + SCHEME_ADDR_SEPARATOR; + ToString(ref builder); - uriStr = (User != null) ? uriStr + User + USER_HOST_SEPARATOR + Host : uriStr + Host; + return builder.ToString(); + } + catch (Exception excp) + { + logger.LogError(excp, "Exception SIPURI ToString. {ErrorMessage}", excp.Message); + throw; + } + finally + { + builder.Dispose(); + } + } - if (Parameters != null && Parameters.Count > 0) + internal void ToString(ref ValueStringBuilder builder) { - uriStr += Parameters.ToString(); + builder.Append(Scheme.ToString()); + builder.Append(SCHEME_ADDR_SEPARATOR); + + if (User != null) + { + builder.Append(User); + builder.Append(USER_HOST_SEPARATOR); } - // If the URI's protocol is not implied already set the transport parameter. - if (Scheme != SIPSchemesEnum.sips && Protocol != SIPProtocolsEnum.udp && !Parameters.Has(m_uriParamTransportKey)) + builder.Append(Host); + + if (Parameters != null && Parameters.Count > 0) { - uriStr += PARAM_TAG_DELIMITER + m_uriParamTransportKey + TAG_NAME_VALUE_SEPERATOR + Protocol.ToString(); + builder.Append(Parameters.ToString()); } - if (Headers != null && Headers.Count > 0) - { - string headerStr = Headers.ToString(); - uriStr += HEADER_START_DELIMITER + headerStr.Substring(1); + // If the URI's protocol is not implied already, set the transport parameter. + if (Scheme != SIPSchemesEnum.sips && + Protocol != SIPProtocolsEnum.udp && + !Parameters.Has(m_uriParamTransportKey)) + { + builder.Append(PARAM_TAG_DELIMITER); + builder.Append(m_uriParamTransportKey); + builder.Append(TAG_NAME_VALUE_SEPERATOR); + builder.Append(Protocol.ToString()); } - return uriStr; - } - catch (Exception excp) + if (Headers != null && Headers.Count > 0) { - logger.LogError(excp, "Exception SIPURI ToString. {ErrorMessage}", excp.Message); - throw; + Headers.ToString(ref builder, HEADER_START_DELIMITER); } } diff --git a/src/core/SIP/SIPUserField.cs b/src/core/SIP/SIPUserField.cs index 6901c3f1ff..5d1146b0d6 100644 --- a/src/core/SIP/SIPUserField.cs +++ b/src/core/SIP/SIPUserField.cs @@ -142,33 +142,38 @@ public static SIPUserField ParseSIPUserField(string userFieldStr) public override string ToString() { + var builder = new ValueStringBuilder(); + try { - string userFieldStr = null; + ToString(ref builder); - if (Name != null) + return builder.ToString(); + } + catch (Exception excp) { - /*if(Regex.Match(Name, @"\s").Success) + logger.LogError(excp, "Exception SIPUserField ToString. {Message}", excp.Message); + throw; + } + finally { - userFieldStr = "\"" + Name + "\" "; + builder.Dispose(); } - else - { - userFieldStr = Name + " "; - }*/ - - userFieldStr = "\"" + Name + "\" "; } - userFieldStr += "<" + URI.ToString() + ">" + Parameters.ToString(); - - return userFieldStr; - } - catch (Exception excp) + internal void ToString(ref ValueStringBuilder builder) + { + if (Name != null) { - logger.LogError(excp, "Exception SIPUserField ToString. {Message}", excp.Message); - throw; + builder.Append("\""); + builder.Append(Name); + builder.Append("\" "); } + + builder.Append('<'); + builder.Append(URI.ToString()); + builder.Append('>'); + builder.Append(Parameters.ToString()); } public string ToParameterlessString() diff --git a/src/net/SDP/SDPAudioVideoMediaFormat.cs b/src/net/SDP/SDPAudioVideoMediaFormat.cs index 34d3ee79a7..6d3378281d 100755 --- a/src/net/SDP/SDPAudioVideoMediaFormat.cs +++ b/src/net/SDP/SDPAudioVideoMediaFormat.cs @@ -38,7 +38,7 @@ public struct SDPAudioVideoMediaFormat public const int DYNAMIC_ID_MAX = 127; public const int DEFAULT_AUDIO_CHANNEL_COUNT = 1; - public static SDPAudioVideoMediaFormat Empty = new SDPAudioVideoMediaFormat() { _isEmpty = true }; + public static SDPAudioVideoMediaFormat Empty = new SDPAudioVideoMediaFormat(); /// /// Indicates whether the format is for audio or video. @@ -116,7 +116,7 @@ public IEnumerable SupportedRtcpFeedbackMessages /// //public string Name { get; set; } - private bool _isEmpty; + private bool _isNotEmpty; /// /// Creates a new SDP media format for a well known media type. Well known type are those that use @@ -130,7 +130,7 @@ public SDPAudioVideoMediaFormat(SDPWellKnownMediaFormatsEnum knownFormat) ID = (int)knownFormat; Rtpmap = null; Fmtp = null; - _isEmpty = false; + _isNotEmpty = true; if (Kind == SDPMediaTypesEnum.audio) { @@ -144,28 +144,20 @@ public SDPAudioVideoMediaFormat(SDPWellKnownMediaFormatsEnum knownFormat) } } - public bool IsH264 - { - get - { - return (Rtpmap ?? "").ToUpperInvariant().Trim().StartsWith("H264"); - } - } + public bool IsH264 => RtmapIs("H264"); - public bool IsMJPEG - { - get - { - return (Rtpmap ?? "").ToUpperInvariant().Trim().StartsWith("JPEG"); - } - } + public bool IsMJPEG => RtmapIs("JPEG"); + + public bool isH265 => RtmapIs("H265"); - public bool isH265 + private bool RtmapIs(string codec) { - get + if (Rtpmap is null) { - return (Rtpmap ?? "").ToUpperInvariant().Trim().StartsWith("H265"); - } + return false; + } + + return Rtpmap.AsSpan().TrimStart().StartsWith(codec.AsSpan(), StringComparison.OrdinalIgnoreCase); } public bool CheckCompatible() @@ -224,7 +216,7 @@ public SDPAudioVideoMediaFormat(SDPMediaTypesEnum kind, int id, string rtpmap, s ID = id; Rtpmap = rtpmap; Fmtp = fmtp; - _isEmpty = false; + _isNotEmpty = true; } /// @@ -246,7 +238,7 @@ public SDPAudioVideoMediaFormat(SDPMediaTypesEnum kind, int id, string name, int ID = id; Rtpmap = null; Fmtp = fmtp; - _isEmpty = false; + _isNotEmpty = true; Rtpmap = SetRtpmap(name, clockRate, channels); } @@ -263,7 +255,7 @@ public SDPAudioVideoMediaFormat(AudioFormat audioFormat) ID = audioFormat.FormatID; Rtpmap = null; Fmtp = audioFormat.Parameters; - _isEmpty = false; + _isNotEmpty = true; Rtpmap = SetRtpmap(audioFormat.FormatName, audioFormat.RtpClockRate, audioFormat.ChannelCount); } @@ -280,7 +272,7 @@ public SDPAudioVideoMediaFormat(VideoFormat videoFormat) ID = videoFormat.FormatID; Rtpmap = null; Fmtp = videoFormat.Parameters; - _isEmpty = false; + _isNotEmpty = true; Rtpmap = SetRtpmap(videoFormat.FormatName, videoFormat.ClockRate); } @@ -291,7 +283,7 @@ public SDPAudioVideoMediaFormat(TextFormat textFormat) ID = textFormat.FormatID; Rtpmap = null; Fmtp = textFormat.Parameters; - _isEmpty = false; + _isNotEmpty = true; Rtpmap = SetRtpmap(textFormat.FormatName, textFormat.ClockRate); } @@ -300,7 +292,7 @@ private string SetRtpmap(string name, int clockRate, int channels = DEFAULT_AUDI ? $"{name}/{clockRate}" : (channels == DEFAULT_AUDIO_CHANNEL_COUNT) ? $"{name}/{clockRate}" : $"{name}/{clockRate}/{channels}"; - public bool IsEmpty() => _isEmpty; + public bool IsEmpty() => !_isNotEmpty; public int ClockRate() { if (Kind == SDPMediaTypesEnum.video) @@ -599,16 +591,20 @@ public static SDPAudioVideoMediaFormat GetCommonRtpEventFormat(ListIf found the matching format or the empty format if not. public static SDPAudioVideoMediaFormat GetFormatForName(List formats, string formatName) { - if (formats == null || formats.Count == 0) + if (formats != null && formats.Count != 0 && formatName != null) { + foreach (var format in formats) + { + if (string.Equals(format.Name(), formatName, StringComparison.OrdinalIgnoreCase)) + { + return format; + } + } + return Empty; } - else - { - return formats.Any(x => x.Name()?.ToLower() == formatName?.ToLower()) ? - formats.First(x => x.Name()?.ToLower() == formatName?.ToLower()) : - Empty; - } + + return Empty; } } } diff --git a/src/net/SDP/SDPConnectionInformation.cs b/src/net/SDP/SDPConnectionInformation.cs index 537dd3950e..dd3d57540e 100644 --- a/src/net/SDP/SDPConnectionInformation.cs +++ b/src/net/SDP/SDPConnectionInformation.cs @@ -15,6 +15,7 @@ using System.Net; using System.Net.Sockets; +using SIPSorcery.Sys; namespace SIPSorcery.Net { @@ -63,5 +64,17 @@ public override string ToString() { return "c=" + ConnectionNetworkType + " " + ConnectionAddressType + " " + ConnectionAddress + m_CRLF; } + + internal void ToString(ref ValueStringBuilder builder) + { + builder.Append("c="); + builder.Append(ConnectionNetworkType); + builder.Append(' '); + builder.Append(ConnectionAddressType); + builder.Append(' '); + builder.Append(ConnectionAddress); + builder.Append(m_CRLF); + + } } } diff --git a/src/net/SDP/SDPMediaAnnouncement.cs b/src/net/SDP/SDPMediaAnnouncement.cs index c31bbde8bc..7a8f31ac56 100755 --- a/src/net/SDP/SDPMediaAnnouncement.cs +++ b/src/net/SDP/SDPMediaAnnouncement.cs @@ -32,6 +32,7 @@ using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; +using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; namespace SIPSorcery.Net @@ -282,87 +283,189 @@ public void ParseMediaFormats(string formatList) public override string ToString() { - string announcement = "m=" + Media + " " + Port + " " + Transport + " " + GetFormatListToString() + m_CRLF; + var builder = new ValueStringBuilder(); - announcement += !string.IsNullOrWhiteSpace(MediaDescription) ? "i=" + MediaDescription + m_CRLF : null; + try + { + ToString(ref builder); - announcement += (Connection == null) ? null : Connection.ToString(); + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) + { + builder.Append("m="); + builder.Append(Media.ToString()); + builder.Append(' '); + builder.Append(Port); + builder.Append(' '); + builder.Append(Transport); + builder.Append(' '); + WriteFormatListString(ref builder); + builder.Append(m_CRLF); + + if (!string.IsNullOrWhiteSpace(MediaDescription)) + { + builder.Append("i="); + builder.Append(MediaDescription); + builder.Append(m_CRLF); + } + + if (Connection != null) + { + builder.Append(Connection.ToString()); + } if (TIASBandwidth > 0) { - announcement += TIAS_BANDWIDTH_ATTRIBUE_PREFIX + TIASBandwidth + m_CRLF; + builder.Append(TIAS_BANDWIDTH_ATTRIBUE_PREFIX); + builder.Append(TIASBandwidth); + builder.Append(m_CRLF); } foreach (string bandwidthAttribute in BandwidthAttributes) { - announcement += "b=" + bandwidthAttribute + m_CRLF; + builder.Append("b="); + builder.Append(bandwidthAttribute); + builder.Append(m_CRLF); + } + + if (!string.IsNullOrWhiteSpace(IceUfrag)) + { + builder.Append("a="); + builder.Append(SDP.ICE_UFRAG_ATTRIBUTE_PREFIX); + builder.Append(':'); + builder.Append(IceUfrag); + builder.Append(m_CRLF); } - announcement += !string.IsNullOrWhiteSpace(IceUfrag) ? "a=" + SDP.ICE_UFRAG_ATTRIBUTE_PREFIX + ":" + IceUfrag + m_CRLF : null; - announcement += !string.IsNullOrWhiteSpace(IcePwd) ? "a=" + SDP.ICE_PWD_ATTRIBUTE_PREFIX + ":" + IcePwd + m_CRLF : null; - announcement += !string.IsNullOrWhiteSpace(DtlsFingerprint) ? "a=" + SDP.DTLS_FINGERPRINT_ATTRIBUTE_PREFIX + ":" + DtlsFingerprint + m_CRLF : null; - announcement += IceRole != null ? $"a={SDP.ICE_SETUP_ATTRIBUTE_PREFIX}:{IceRole}{m_CRLF}" : null; + if (!string.IsNullOrWhiteSpace(IcePwd)) + { + builder.Append("a="); + builder.Append(SDP.ICE_PWD_ATTRIBUTE_PREFIX); + builder.Append(':'); + builder.Append(IcePwd); + builder.Append(m_CRLF); + } + + if (!string.IsNullOrWhiteSpace(DtlsFingerprint)) + { + builder.Append("a="); + builder.Append(SDP.DTLS_FINGERPRINT_ATTRIBUTE_PREFIX); + builder.Append(':'); + builder.Append(DtlsFingerprint); + builder.Append(m_CRLF); + } + + if (IceRole != null) + { + builder.Append("a="); + builder.Append(SDP.ICE_SETUP_ATTRIBUTE_PREFIX); + builder.Append(':'); + builder.Append(IceRole.ToString()); + builder.Append(m_CRLF); + } - if (IceCandidates?.Count() > 0) + if (IceCandidates?.Any() == true) { foreach (var candidate in IceCandidates) { - announcement += $"a={SDP.ICE_CANDIDATE_ATTRIBUTE_PREFIX}:{candidate}{m_CRLF}"; + builder.Append("a="); + builder.Append(SDP.ICE_CANDIDATE_ATTRIBUTE_PREFIX); + builder.Append(':'); + builder.Append(candidate); + builder.Append(m_CRLF); } } if (IceOptions != null) { - announcement += $"a={SDP.ICE_OPTIONS}:" + IceOptions + m_CRLF; + builder.Append("a="); + builder.Append(SDP.ICE_OPTIONS); + builder.Append(':'); + builder.Append(IceOptions); + builder.Append(m_CRLF); } if (IceEndOfCandidates) { - announcement += $"a={SDP.END_ICE_CANDIDATES_ATTRIBUTE}" + m_CRLF; + builder.Append("a="); + builder.Append(SDP.END_ICE_CANDIDATES_ATTRIBUTE); + builder.Append(m_CRLF); } - announcement += !string.IsNullOrWhiteSpace(MediaID) ? "a=" + SDP.MEDIA_ID_ATTRIBUTE_PREFIX + ":" + MediaID + m_CRLF : null; + if (!string.IsNullOrWhiteSpace(MediaID)) + { + builder.Append("a="); + builder.Append(SDP.MEDIA_ID_ATTRIBUTE_PREFIX); + builder.Append(':'); + builder.Append(MediaID); + builder.Append(m_CRLF); + } - announcement += GetFormatListAttributesToString(); + builder.Append(GetFormatListAttributesToString()); + + foreach (var ext in HeaderExtensions) + { + builder.Append(MEDIA_EXTENSION_MAP_ATTRIBUE_PREFIX); + builder.Append(ext.Value.Id); + builder.Append(' '); + builder.Append(ext.Value.Uri); + builder.Append(m_CRLF); + } - announcement += string.Join("", HeaderExtensions.Select(x => $"{MEDIA_EXTENSION_MAP_ATTRIBUE_PREFIX}{x.Value.Id} {x.Value.Uri}{m_CRLF}")); foreach (string extra in ExtraMediaAttributes) { - announcement += string.IsNullOrWhiteSpace(extra) ? null : extra + m_CRLF; + if (!string.IsNullOrWhiteSpace(extra)) + { + builder.Append(extra); + builder.Append(m_CRLF); + } } foreach (SDPSecurityDescription desc in this.SecurityDescriptions) { - announcement += desc.ToString() + m_CRLF; + builder.Append(desc.ToString()); + builder.Append(m_CRLF); } if (MediaStreamStatus != null) { - announcement += MediaStreamStatusType.GetAttributeForMediaStreamStatus(MediaStreamStatus.Value) + m_CRLF; + builder.Append(MediaStreamStatusType.GetAttributeForMediaStreamStatus(MediaStreamStatus.Value)); + builder.Append(m_CRLF); } if (SsrcGroupID != null && SsrcAttributes.Count > 0) { - announcement += MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_PREFIX + SsrcGroupID; + builder.Append(MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_PREFIX); + builder.Append(SsrcGroupID); foreach (var ssrcAttr in SsrcAttributes) { - announcement += $" {ssrcAttr.SSRC}"; + builder.Append(' '); + builder.Append(ssrcAttr.SSRC); } - announcement += m_CRLF; + builder.Append(m_CRLF); } if (SsrcAttributes.Count > 0) { foreach (var ssrcAttr in SsrcAttributes) { + builder.Append(MEDIA_FORMAT_SSRC_ATTRIBUE_PREFIX); + builder.Append(ssrcAttr.SSRC); if (!string.IsNullOrWhiteSpace(ssrcAttr.Cname)) { - announcement += $"{MEDIA_FORMAT_SSRC_ATTRIBUE_PREFIX}{ssrcAttr.SSRC} {SDPSsrcAttribute.MEDIA_CNAME_ATTRIBUE_PREFIX}:{ssrcAttr.Cname}" + m_CRLF; - } - else - { - announcement += $"{MEDIA_FORMAT_SSRC_ATTRIBUE_PREFIX}{ssrcAttr.SSRC}" + m_CRLF; + builder.Append(' '); + builder.Append(SDPSsrcAttribute.MEDIA_CNAME_ATTRIBUE_PREFIX); + builder.Append(':'); + builder.Append(ssrcAttr.Cname); } + builder.Append(m_CRLF); } } @@ -371,50 +474,87 @@ public override string ToString() // an application sets it then it's likely to be for a specific reason. if (SctpMap != null) { - announcement += $"{MEDIA_FORMAT_SCTP_MAP_ATTRIBUE_PREFIX}{SctpMap}" + m_CRLF; + builder.Append(MEDIA_FORMAT_SCTP_MAP_ATTRIBUE_PREFIX); + builder.Append(SctpMap); + builder.Append(m_CRLF); } else { if (SctpPort != null) { - announcement += $"{MEDIA_FORMAT_SCTP_PORT_ATTRIBUE_PREFIX}{SctpPort}" + m_CRLF; + builder.Append(MEDIA_FORMAT_SCTP_PORT_ATTRIBUE_PREFIX); + builder.Append(SctpPort); + builder.Append(m_CRLF); } if (MaxMessageSize != 0) { - announcement += $"{MEDIA_FORMAT_MAX_MESSAGE_SIZE_ATTRIBUE_PREFIX}{MaxMessageSize}" + m_CRLF; + builder.Append(MEDIA_FORMAT_MAX_MESSAGE_SIZE_ATTRIBUE_PREFIX); + builder.Append(MaxMessageSize); + builder.Append(m_CRLF); + } } } - return announcement; + public string GetFormatListToString() + { + if (Media == SDPMediaTypesEnum.message) + { + return "*"; } - public string GetFormatListToString() + var builder = new ValueStringBuilder(); + + try { + WriteFormatListString(ref builder); + if (Media == SDPMediaTypesEnum.application) { - StringBuilder sb = new StringBuilder(); - foreach (var appFormat in ApplicationMediaFormats) + return builder.ToString(); + } + else + { + return builder.Length > 0 ? builder.ToString() : null; + } + } + finally { - sb.Append(appFormat.Key); - sb.Append(" "); + builder.Dispose(); + } } - return sb.ToString().Trim(); + internal void WriteFormatListString(ref ValueStringBuilder builder) + { + if (Media == SDPMediaTypesEnum.message) + { + builder.Append('*'); } - else if (Media == SDPMediaTypesEnum.message) + else if (Media == SDPMediaTypesEnum.application) { - return "*"; + var first = true; + foreach (var appFormat in ApplicationMediaFormats) + { + if (!first) + { + builder.Append(' '); + } + builder.Append(appFormat.Key); + first = false; + } } else { - string mediaFormatList = null; + var first = true; foreach (var mediaFormat in MediaFormats) { - mediaFormatList += mediaFormat.Key + " "; + if (!first) + { + builder.Append(' '); + } + builder.Append(mediaFormat.Key); + first = false; } - - return (mediaFormatList != null) ? mediaFormatList.Trim() : null; } } diff --git a/src/net/SDP/SDPSecurityDescription.cs b/src/net/SDP/SDPSecurityDescription.cs index a8025e57d9..e7f3434ef1 100644 --- a/src/net/SDP/SDPSecurityDescription.cs +++ b/src/net/SDP/SDPSecurityDescription.cs @@ -388,7 +388,7 @@ private static void parseKeySaltBase64(CryptoSuites cryptoSuite, string base64Ke private static void checkValidKeyInfoCharacters(string keyParameter, string keyInfo) { - foreach (char c in keyInfo.ToCharArray()) + foreach (char c in keyInfo.AsSpan()) { if (c < 0x21 || c > 0x7e) { diff --git a/src/net/STUN/STUNAttributes/STUNAddressAttribute.cs b/src/net/STUN/STUNAttributes/STUNAddressAttribute.cs index ff00d9ed72..0ccf839dde 100755 --- a/src/net/STUN/STUNAttributes/STUNAddressAttribute.cs +++ b/src/net/STUN/STUNAttributes/STUNAddressAttribute.cs @@ -122,11 +122,12 @@ public override int ToByteBuffer(byte[] buffer, int startIndex) return STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + ADDRESS_ATTRIBUTE_IPV4_LENGTH; } - public override string ToString() + private protected override void ValueToString(ref ValueStringBuilder sb) { - string attrDescrStr = "STUN Attribute: " + base.AttributeType + ", address=" + Address.ToString() + ", port=" + Port + "."; - - return attrDescrStr; + sb.Append("Address="); + sb.Append(Address.ToString()); + sb.Append(", Port="); + sb.Append(Port); } } } diff --git a/src/net/STUN/STUNAttributes/STUNAddressAttributeBase.cs b/src/net/STUN/STUNAttributes/STUNAddressAttributeBase.cs index 6fea343082..1f95a3f5fb 100644 --- a/src/net/STUN/STUNAttributes/STUNAddressAttributeBase.cs +++ b/src/net/STUN/STUNAttributes/STUNAddressAttributeBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net; using System.Text; +using SIPSorcery.Sys; namespace SIPSorcery.Net { @@ -37,5 +38,15 @@ public STUNAddressAttributeBase(STUNAttributeTypesEnum attributeType, byte[] val : base(attributeType, value) { } + + private protected override void ValueToString(ref ValueStringBuilder sb) + { + sb.Append("Address="); + sb.Append(Address.ToString()); + sb.Append(", Port="); + sb.Append(Port); + sb.Append(", Family="); + sb.Append(Family switch { 1 => "IPV4", 2 => "IPV6", _ => "Invalid", }); + } } } diff --git a/src/net/STUN/STUNAttributes/STUNAttribute.cs b/src/net/STUN/STUNAttributes/STUNAttribute.cs index 1d84d78151..e1ebac04d9 100644 --- a/src/net/STUN/STUNAttributes/STUNAttribute.cs +++ b/src/net/STUN/STUNAttributes/STUNAttribute.cs @@ -272,11 +272,36 @@ public virtual int ToByteBuffer(byte[] buffer, int startIndex) return STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + PaddedLength; } - public new virtual string ToString() + public override string ToString() { - string attrDescrString = "STUN Attribute: " + AttributeType.ToString() + ", length=" + PaddedLength + "."; + var sb = new ValueStringBuilder(stackalloc char[256]); - return attrDescrString; + try + { + ToString(ref sb); + + return sb.ToString(); + } + finally + { + sb.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder sb) + { + sb.Append("STUN Attribute: "); + sb.Append(AttributeType.ToString()); + sb.Append(", "); + ValueToString(ref sb); + sb.Append('.'); + } + + private protected virtual void ValueToString(ref ValueStringBuilder sb) + { + sb.Append(Value); + sb.Append(", length="); + sb.Append(PaddedLength); } } } diff --git a/src/net/STUN/STUNAttributes/STUNChangeRequestAttribute.cs b/src/net/STUN/STUNAttributes/STUNChangeRequestAttribute.cs index fff4156578..dd7378943f 100644 --- a/src/net/STUN/STUNAttributes/STUNChangeRequestAttribute.cs +++ b/src/net/STUN/STUNAttributes/STUNChangeRequestAttribute.cs @@ -14,6 +14,7 @@ //----------------------------------------------------------------------------- using System; +using SIPSorcery.Sys; namespace SIPSorcery.Net { @@ -51,11 +52,14 @@ public STUNChangeRequestAttribute(byte[] attributeValue) } } - public override string ToString() + private protected override void ValueToString(ref ValueStringBuilder sb) { - string attrDescrStr = "STUN Attribute: " + STUNAttributeTypesEnum.ChangeRequest.ToString() + ", key byte=" + m_changeRequestByte.ToString("X") + ", change address=" + ChangeAddress + ", change port=" + ChangePort + "."; - - return attrDescrStr; + sb.Append("key byte="); + sb.Append(m_changeRequestByte, "X"); + sb.Append(", change address="); + sb.Append(ChangeAddress); + sb.Append(", change port="); + sb.Append(ChangePort); } } } diff --git a/src/net/STUN/STUNAttributes/STUNConnectionIdAttribute.cs b/src/net/STUN/STUNAttributes/STUNConnectionIdAttribute.cs index 33114ab83f..eced72b610 100644 --- a/src/net/STUN/STUNAttributes/STUNConnectionIdAttribute.cs +++ b/src/net/STUN/STUNAttributes/STUNConnectionIdAttribute.cs @@ -45,11 +45,10 @@ public STUNConnectionIdAttribute(uint connectionId) ConnectionId = connectionId; } - public override string ToString() + private protected override void ValueToString(ref ValueStringBuilder sb) { - string attrDescrStr = "STUN CONNECTION_ID Attribute: value=" + ConnectionId + "."; - - return attrDescrStr; + sb.Append("connection ID="); + sb.Append(ConnectionId); } } } diff --git a/src/net/STUN/STUNAttributes/STUNErrorCodeAttribute.cs b/src/net/STUN/STUNAttributes/STUNErrorCodeAttribute.cs index b97bedb815..ee893f563d 100755 --- a/src/net/STUN/STUNAttributes/STUNErrorCodeAttribute.cs +++ b/src/net/STUN/STUNAttributes/STUNErrorCodeAttribute.cs @@ -15,6 +15,7 @@ using System; using System.Text; +using SIPSorcery.Sys; namespace SIPSorcery.Net { @@ -61,11 +62,12 @@ public override int ToByteBuffer(byte[] buffer, int startIndex) return STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + 4 + reasonPhraseBytes.Length; } - public override string ToString() + private protected override void ValueToString(ref ValueStringBuilder sb) { - string attrDescrStr = "STUN ERROR_CODE_ADDRESS Attribute: error code=" + ErrorCode + ", reason phrase=" + ReasonPhrase + "."; - - return attrDescrStr; + sb.Append("error code="); + sb.Append(ErrorCode); + sb.Append(", reason phrase="); + sb.Append(ReasonPhrase); } } } diff --git a/src/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs b/src/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs index c6cc025c0e..21c0f0eed2 100644 --- a/src/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs +++ b/src/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs @@ -173,5 +173,7 @@ public IPEndPoint GetIPEndPoint() return null; } } + + private protected override void ValueToString(ref ValueStringBuilder sb) => base.ValueToString(ref sb); } } diff --git a/src/net/STUN/STUNDns.cs b/src/net/STUN/STUNDns.cs index 7e316ea601..4598912539 100644 --- a/src/net/STUN/STUNDns.cs +++ b/src/net/STUN/STUNDns.cs @@ -185,7 +185,7 @@ private static async Task Resolve(STUNUri uri, QueryType queryType) { ServiceHostEntry srvResult = null; // No explicit port so use a SRV -> (A | AAAA -> A) record lookup. - var result = await _lookupClient.ResolveServiceAsync(uri.Host, uri.Scheme.ToString(), uri.Protocol.ToString().ToLower()).ConfigureAwait(false); + var result = await _lookupClient.ResolveServiceAsync(uri.Host, uri.Scheme.ToString(), uri.Protocol.ToLowerString()).ConfigureAwait(false); if (result == null || result.Count() == 0) { //logger.LogDebug("STUNDns SRV lookup returned no results for {uri}.", uri); diff --git a/src/sys/CRC32.cs b/src/sys/CRC32.cs index 52df73bd51..ff89a9008d 100644 --- a/src/sys/CRC32.cs +++ b/src/sys/CRC32.cs @@ -1,6 +1,7 @@ // from http://damieng.com/blog/2006/08/08/Calculating_CRC32_in_C_and_NET using System; +using System.Buffers.Binary; using System.Security.Cryptography; namespace SIPSorcery.Sys @@ -36,7 +37,18 @@ public override void Initialize() protected override void HashCore(byte[] buffer, int start, int length) { - hash = CalculateHash(table, hash, buffer, start, length); + hash = CalculateHash(table, hash, buffer.AsSpan(start, length)); + } + + protected +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER || NET5_0_OR_GREATER + override +#else + virtual +#endif + void HashCore(ReadOnlySpan buffer) + { + hash = CalculateHash(table, hash, buffer); } protected override byte[] HashFinal() @@ -53,17 +65,32 @@ public override int HashSize public static UInt32 Compute(byte[] buffer) { - return ~CalculateHash(InitializeTable(DefaultPolynomial), DefaultSeed, buffer, 0, buffer.Length); + return Compute(buffer.AsSpan(0, buffer.Length)); } public static UInt32 Compute(UInt32 seed, byte[] buffer) { - return ~CalculateHash(InitializeTable(DefaultPolynomial), seed, buffer, 0, buffer.Length); + return Compute(seed, buffer.AsSpan()); } public static UInt32 Compute(UInt32 polynomial, UInt32 seed, byte[] buffer) { - return ~CalculateHash(InitializeTable(polynomial), seed, buffer, 0, buffer.Length); + return Compute(polynomial, seed, buffer.AsSpan()); + } + + public static uint Compute(ReadOnlySpan buffer) + { + return ~CalculateHash(InitializeTable(DefaultPolynomial), DefaultSeed, buffer); + } + + public static uint Compute(uint seed, ReadOnlySpan buffer) + { + return ~CalculateHash(InitializeTable(DefaultPolynomial), seed, buffer); + } + + public static uint Compute(uint polynomial, uint seed, ReadOnlySpan buffer) + { + return ~CalculateHash(InitializeTable(polynomial), seed, buffer); } private static UInt32[] InitializeTable(UInt32 polynomial) @@ -99,27 +126,31 @@ private static UInt32[] InitializeTable(UInt32 polynomial) return createTable; } - private static UInt32 CalculateHash(UInt32[] table, UInt32 seed, byte[] buffer, int start, int size) + private static UInt32 CalculateHash(ReadOnlySpan table, uint seed, ReadOnlySpan buffer) { - UInt32 crc = seed; - for (int i = start; i < size; i++) + /* + if (Sse42.IsSupported) + { +    uint crc = Sse42.Crc32(seed, value); + } + */ + + var crc = seed; + for (int i = 0; i < buffer.Length; i++) { unchecked { - crc = (crc >> 8) ^ table[buffer[i] ^ crc & 0xff]; + crc = (crc >> 8) ^ table[buffer[i] ^ (byte)(crc & 0xff)]; } } return crc; } - private byte[] UInt32ToBigEndianBytes(UInt32 x) + private byte[] UInt32ToBigEndianBytes(uint x) { - return new byte[] { - (byte)((x >> 24) & 0xff), - (byte)((x >> 16) & 0xff), - (byte)((x >> 8) & 0xff), - (byte)(x & 0xff) - }; + var result = new byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32BigEndian(result, x); + return result; } } } diff --git a/src/sys/EncodingExtensions.cs b/src/sys/EncodingExtensions.cs new file mode 100644 index 0000000000..c285be0cf0 --- /dev/null +++ b/src/sys/EncodingExtensions.cs @@ -0,0 +1,57 @@ +using System; +using System.Text; + +namespace SIPSorcery.Sys +{ + /// + /// Extension methods for . + /// + internal static class EncodingExtensions + { +#if NETSTANDARD2_0 || NETFRAMEWORK + /// + /// Decodes a sequence of bytes from a read-only span into a string. + /// + /// The encoding to use for the conversion. + /// The span containing the sequence of bytes to decode. + /// A string containing the decoded characters. + public unsafe static string GetString(this Encoding encoding, ReadOnlySpan bytes) + { + fixed (byte* ptr = bytes) + { + return encoding.GetString(ptr, bytes.Length); + } + } + + /// + /// Encodes a set of characters from a read-only span into a sequence of bytes. + /// + /// The encoding to use for the conversion. + /// The span containing the set of characters to encode. + /// The span to contain the resulting sequence of bytes. + /// The actual number of bytes written into the byte span. + public unsafe static int GetBytes(this Encoding encoding, ReadOnlySpan chars, Span bytes) + { + fixed (char* pChars = chars) + fixed (byte* pBytes = bytes) + { + return encoding.GetBytes(pChars, chars.Length, pBytes, bytes.Length); + } + } + + /// + /// Calculates the number of bytes needed to encode a set of characters. + /// + /// The encoding to use for the calculation. + /// The span containing the set of characters to encode. + /// The number of bytes needed to encode the specified characters. + public unsafe static int GetByteCount(this Encoding encoding, ReadOnlySpan chars) + { + fixed (char* pChars = chars) + { + return encoding.GetByteCount(pChars, chars.Length); + } + } +#endif + } +} diff --git a/src/sys/EnumExtensions.cs b/src/sys/EnumExtensions.cs new file mode 100644 index 0000000000..8cacca3f0a --- /dev/null +++ b/src/sys/EnumExtensions.cs @@ -0,0 +1,55 @@ +using System.Net.Sockets; + +namespace SIPSorcery.Sys; + +/// +/// Extension methods for enumeration types used in the system. +/// +internal static class EnumExtensions +{ + /// + /// Converts a ProtocolType enumeration value to its lowercase string representation. + /// + /// The ProtocolType enumeration value to convert. + /// A lowercase string representation of the protocol type. For most protocols, + /// returns the standard abbreviated form (e.g. "tcp", "udp", "ipv6"). For unrecognized + /// protocol types, returns the enum value converted to lowercase. + /// + /// This method provides standardized string representations for network protocols, + /// particularly useful for logging, configuration, and protocol-specific formatting needs. + /// + public static string ToLowerString(this ProtocolType protocolType) + { + return protocolType switch + { + ProtocolType.IP => "ip", + + ProtocolType.Icmp => "icmp", + ProtocolType.Igmp => "igmp", + ProtocolType.Ggp => "ggp", + + ProtocolType.IPv4 => "ipv4", + ProtocolType.Tcp => "tcp", + ProtocolType.Pup => "pup", + ProtocolType.Udp => "udp", + ProtocolType.Idp => "idp", + ProtocolType.IPv6 => "ipv6", + ProtocolType.IPv6RoutingHeader => "routing", + ProtocolType.IPv6FragmentHeader => "fragment", + ProtocolType.IPSecEncapsulatingSecurityPayload => "ipsecencapsulatingsecuritypayload", + ProtocolType.IPSecAuthenticationHeader => "ipsecauthenticationheader", + ProtocolType.IcmpV6 => "icmpv6", + ProtocolType.IPv6NoNextHeader => "nonext", + ProtocolType.IPv6DestinationOptions => "dstopts", + ProtocolType.ND => "nd", + ProtocolType.Raw => "raw", + + ProtocolType.Ipx => "ipx", + ProtocolType.Spx => "spx", + ProtocolType.SpxII => "spx2", + ProtocolType.Unknown => "unknown", + + _ => protocolType.ToString().ToLowerInvariant() + }; + } +} diff --git a/src/sys/JSONWriter.cs b/src/sys/JSONWriter.cs index 50c5d2ab09..4435504729 100644 --- a/src/sys/JSONWriter.cs +++ b/src/sys/JSONWriter.cs @@ -15,11 +15,13 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Runtime.Serialization; using System.Text; +using SIPSorcery.Sys; namespace TinyJson { @@ -31,203 +33,250 @@ public static class JSONWriter { public static string ToJson(this object item) { - StringBuilder stringBuilder = new StringBuilder(); - AppendValue(stringBuilder, item); - return stringBuilder.ToString(); + var builder = new ValueStringBuilder(); + + try + { + AppendValue(ref builder, item); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } } - static void AppendValue(StringBuilder stringBuilder, object item) + static void AppendValue(ref ValueStringBuilder builder, object item) { if (item == null) { - stringBuilder.Append("null"); + builder.Append("null"); return; } - Type type = item.GetType(); - if (type == typeof(string) || type == typeof(char)) + var type = item.GetType(); + + if (type.IsEnum) { - stringBuilder.Append('"'); - string str = item.ToString(); - for (int i = 0; i < str.Length; ++i) - { - if (str[i] < ' ' || str[i] == '"' || str[i] == '\\') + builder.Append('"'); + builder.Append(item.ToString()); + builder.Append('"'); + return; + } + + var typeCode = Type.GetTypeCode(type); + + switch (typeCode) + { + + case TypeCode.String: { - stringBuilder.Append('\\'); - int j = "\"\\\n\r\t\b\f".IndexOf(str[i]); - if (j >= 0) + builder.Append('"'); + var str = ((string)item).AsSpan(); + for (var i = 0; i < str.Length; i++) { - stringBuilder.Append("\"\\nrtbf"[j]); - } - else - { - stringBuilder.AppendFormat("u{0:X4}", (UInt32)str[i]); + AppendEscapedChar(ref builder, str[i]); } + builder.Append('"'); + return; } - else + + case TypeCode.Char: { - stringBuilder.Append(str[i]); + builder.Append('"'); + AppendEscapedChar(ref builder, (char)item); + builder.Append('"'); + return; + + } + + case TypeCode.Boolean: + { + builder.Append((bool)item ? "true" : "false"); + return; + } + + case TypeCode.Single: + { + builder.Append((float)item, provider: System.Globalization.CultureInfo.InvariantCulture); + return; + } + + case TypeCode.Double: + { + builder.Append((double)item, provider: System.Globalization.CultureInfo.InvariantCulture); + return; + } + + case TypeCode.Decimal: + { + builder.Append((decimal)item, provider: System.Globalization.CultureInfo.InvariantCulture); + return; + } + + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.UInt16: + case TypeCode.Int32: + case TypeCode.UInt32: + case TypeCode.Int64: + case TypeCode.UInt64: + { + builder.Append(item.ToString()); + return; + } + + case TypeCode.DBNull: + case TypeCode.Empty: + { + builder.Append("null"); + return; } - } - stringBuilder.Append('"'); - } - else if (type == typeof(byte) || type == typeof(sbyte)) - { - stringBuilder.Append(item.ToString()); - } - else if (type == typeof(short) || type == typeof(ushort)) - { - stringBuilder.Append(item.ToString()); - } - else if (type == typeof(int) || type == typeof(uint)) - { - stringBuilder.Append(item.ToString()); - } - else if (type == typeof(long) || type == typeof(ulong)) - { - stringBuilder.Append(item.ToString()); - } - else if (type == typeof(float)) - { - stringBuilder.Append(((float)item).ToString(System.Globalization.CultureInfo.InvariantCulture)); - } - else if (type == typeof(double)) - { - stringBuilder.Append(((double)item).ToString(System.Globalization.CultureInfo.InvariantCulture)); - } - else if (type == typeof(decimal)) - { - stringBuilder.Append(((decimal)item).ToString(System.Globalization.CultureInfo.InvariantCulture)); - } - else if (type == typeof(bool)) - { - stringBuilder.Append(((bool)item) ? "true" : "false"); } - else if (type.IsEnum) + + static void AppendEscapedChar(ref ValueStringBuilder builder, char ch) { - stringBuilder.Append('"'); - stringBuilder.Append(item.ToString()); - stringBuilder.Append('"'); + if (ch is >= ' ' and not '"' and not '\\') + { + builder.Append(ch); + } + else + { + + builder.Append('\\'); + switch (ch) + { + case '"': builder.Append('"'); break; + case '\\': builder.Append('\\'); break; + case '\n': builder.Append('n'); break; + case '\r': builder.Append('r'); break; + case '\t': builder.Append('t'); break; + case '\b': builder.Append('b'); break; + case '\f': builder.Append('f'); break; + default: + builder.Append('u'); + builder.Append(((uint)ch).ToString("X4")); + break; + } + } } - else if (item is IList) + + if (item is IList list) { - stringBuilder.Append('['); - bool isFirst = true; - IList list = item as IList; - for (int i = 0; i < list.Count; i++) + builder.Append('['); + var isFirst = true; + for (var i = 0; i < list.Count; i++) { - if (isFirst) + if (!isFirst) { - isFirst = false; + builder.Append(','); } else { - stringBuilder.Append(','); + isFirst = false; } - AppendValue(stringBuilder, list[i]); + AppendValue(ref builder, list[i]); } - stringBuilder.Append(']'); + builder.Append(']'); } else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { - Type keyType = type.GetGenericArguments()[0]; - - //Refuse to output dictionary keys that aren't of type string + var keyType = type.GetGenericArguments()[0]; if (keyType != typeof(string)) { - stringBuilder.Append("{}"); + builder.Append("{}"); return; } - stringBuilder.Append('{'); - IDictionary dict = item as IDictionary; - bool isFirst = true; - foreach (object key in dict.Keys) + var dict = item as IDictionary; + builder.Append('{'); + var isFirst = true; + foreach (var key in dict.Keys) { - if (isFirst) + if (!isFirst) { - isFirst = false; + builder.Append(','); } else { - stringBuilder.Append(','); + isFirst = false; } - stringBuilder.Append('\"'); - stringBuilder.Append((string)key); - stringBuilder.Append("\":"); - AppendValue(stringBuilder, dict[key]); + builder.Append('\"'); + builder.Append((string)key); + builder.Append("\":"); + AppendValue(ref builder, dict[key]); } - stringBuilder.Append('}'); + builder.Append('}'); } else { - stringBuilder.Append('{'); + builder.Append('{'); + var isFirst = true; - bool isFirst = true; - FieldInfo[] fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); - for (int i = 0; i < fieldInfos.Length; i++) + var fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); + foreach (var field in fieldInfos) { - if (fieldInfos[i].IsDefined(typeof(IgnoreDataMemberAttribute), true)) + if (field.IsDefined(typeof(IgnoreDataMemberAttribute), true)) { continue; } - object value = fieldInfos[i].GetValue(item); + var value = field.GetValue(item); if (value != null) { - if (isFirst) + if (!isFirst) { - isFirst = false; + builder.Append(','); } else { - stringBuilder.Append(','); + isFirst = false; } - stringBuilder.Append('\"'); - stringBuilder.Append(GetMemberName(fieldInfos[i])); - stringBuilder.Append("\":"); - AppendValue(stringBuilder, value); + builder.Append('\"'); + builder.Append(GetMemberName(field)); + builder.Append("\":"); + AppendValue(ref builder, value); } } - PropertyInfo[] propertyInfo = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); - for (int i = 0; i < propertyInfo.Length; i++) + + var propertyInfos = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); + foreach (var prop in propertyInfos) { - if (!propertyInfo[i].CanRead || propertyInfo[i].IsDefined(typeof(IgnoreDataMemberAttribute), true)) + if (!prop.CanRead || prop.IsDefined(typeof(IgnoreDataMemberAttribute), true)) { continue; } - object value = propertyInfo[i].GetValue(item, null); + var value = prop.GetValue(item, null); if (value != null) { - if (isFirst) + if (!isFirst) { - isFirst = false; + builder.Append(','); } else { - stringBuilder.Append(','); + isFirst = false; } - stringBuilder.Append('\"'); - stringBuilder.Append(GetMemberName(propertyInfo[i])); - stringBuilder.Append("\":"); - AppendValue(stringBuilder, value); + builder.Append('\"'); + builder.Append(GetMemberName(prop)); + builder.Append("\":"); + AppendValue(ref builder, value); } } - stringBuilder.Append('}'); + builder.Append('}'); } } static string GetMemberName(MemberInfo member) { - if (member.IsDefined(typeof(DataMemberAttribute), true)) + if (Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true) is DataMemberAttribute attr && + !string.IsNullOrEmpty(attr.Name)) { - DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); - if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) - { - return dataMemberAttribute.Name; - } + return attr.Name; } return member.Name; diff --git a/src/sys/TypeExtensions.cs b/src/sys/TypeExtensions.cs index 47d7e29998..cfadfc42c6 100755 --- a/src/sys/TypeExtensions.cs +++ b/src/sys/TypeExtensions.cs @@ -16,6 +16,7 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; using System.Net; @@ -55,7 +56,7 @@ public static class TypeExtensions /// public static bool IsNullOrBlank(this string s) { - if (s == null || s.Trim(WhiteSpaceChars).Length == 0) + if (s == null || s.AsSpan().Trim(WhiteSpaceChars).Length == 0) { return true; } @@ -65,7 +66,7 @@ public static bool IsNullOrBlank(this string s) public static bool NotNullOrBlank(this string s) { - if (s == null || s.Trim(WhiteSpaceChars).Length == 0) + if (s == null || s.AsSpan().Trim(WhiteSpaceChars).Length == 0) { return false; } @@ -123,59 +124,29 @@ public static string Slice(this string s, char startDelimiter, char endDelimeter public static string HexStr(this byte[] buffer, char? separator = null) { - return buffer.HexStr(buffer.Length, separator); + return HexStr(buffer.AsSpan(), separator: separator, lowercase: false); } public static string HexStr(this byte[] buffer, int length, char? separator = null) { - if (separator is { } s) - { - int numberOfChars = length * 3 - 1; -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - return string.Create(numberOfChars, (buffer, length, s), PopulateNewStringWithSeparator); -#else - var rv = new char[numberOfChars]; - PopulateNewStringWithSeparator(rv, (buffer, length, s)); - return new string(rv); -#endif - } - else - { - int numberOfChars = length * 2; -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - return string.Create(numberOfChars, (buffer, length), PopulateNewStringWithoutSeparator); -#else - var rv = new char[numberOfChars]; - PopulateNewStringWithoutSeparator(rv, (buffer, length)); - return new string(rv); -#endif - } + return HexStr(buffer.AsSpan(0, buffer.Length), separator: separator, lowercase: false); + } - static void PopulateNewStringWithSeparator(Span chars, (byte[] buffer, int length, char separator) state) - { - var (buffer, length, s) = state; - for (int i = 0, j = 0; i < length; i++) - { - var val = buffer[i]; - chars[j++] = char.ToUpperInvariant(hexmap[val >> 4]); - chars[j++] = char.ToUpperInvariant(hexmap[val & 15]); - if (j < chars.Length) - { - chars[j++] = s; - } - } - } + public static string HexStr(this byte[] buffer, int length, char? separator = null, bool lowercase = false) + { + return HexStr(buffer.AsSpan(0, length), separator: separator, lowercase: lowercase); + } - static void PopulateNewStringWithoutSeparator(Span chars, (byte[] buffer, int length) state) - { - var (buffer, length) = state; - for (int i = 0, j = 0; i < length; i++) - { - var val = buffer[i]; - chars[j++] = char.ToUpperInvariant(hexmap[val >> 4]); - chars[j++] = char.ToUpperInvariant(hexmap[val & 15]); - } - } + public static string HexStr(this Span buffer, char? separator = null, bool lowercase = false) + { + return HexStr((ReadOnlySpan)buffer, separator: separator, lowercase: lowercase); + } + + public static string HexStr(this ReadOnlySpan buffer, char? separator = null, bool lowercase = false) + { + using var sb = new ValueStringBuilder(stackalloc char[256]); + sb.Append(buffer, separator, lowercase); + return sb.ToString(); } public static byte[] ParseHexStr(string hexStr) diff --git a/src/sys/ValueStringBuilder.AppendSpanFormattable.cs b/src/sys/ValueStringBuilder.AppendSpanFormattable.cs new file mode 100644 index 0000000000..9498a559fa --- /dev/null +++ b/src/sys/ValueStringBuilder.AppendSpanFormattable.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.CompilerServices; + +namespace SIPSorcery.Sys +{ +#if NET6_0_OR_GREATER + internal ref partial struct ValueStringBuilder + { + /// + /// Appends a value that implements ISpanFormattable to the string builder using span-based formatting. + /// If span formatting fails, falls back to regular string formatting. + /// + /// The type of the value to format. Must implement ISpanFormattable. + /// The value to append. + /// A format string that defines the formatting to apply, or null to use default formatting. + /// An object that supplies culture-specific formatting information, or null to use default formatting. + internal void AppendSpanFormattable(T value, string? format = null, IFormatProvider? provider = null) where T : ISpanFormattable + { + if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider)) + { + _pos += charsWritten; + } + else + { + Append(value.ToString(format, provider)); + } + } + } +#endif +} diff --git a/src/sys/ValueStringBuilder.Bytes.cs b/src/sys/ValueStringBuilder.Bytes.cs new file mode 100644 index 0000000000..ccf0da837b --- /dev/null +++ b/src/sys/ValueStringBuilder.Bytes.cs @@ -0,0 +1,75 @@ +using System; +using System.Runtime.InteropServices; + +namespace SIPSorcery.Sys +{ + internal ref partial struct ValueStringBuilder + { + /// + /// Character array for uppercase hexadecimal representation (0-9, A-F). + /// + private static readonly char[] upperHexmap = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + /// + /// Character array for lowercase hexadecimal representation (0-9, a-f). + /// + private static readonly char[] lowerHexmap = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + /// + /// Appends a byte array to the string builder as hexadecimal characters. + /// + /// The byte array to append. Can be null. + /// Optional separator character to insert between bytes. + public void Append(byte[]? bytes, char? separator = null) + { + if (bytes is { Length: > 0 }) + { + Append(bytes.AsSpan(), separator); + } + } + + /// + /// Appends a span of bytes to the string builder as hexadecimal characters. + /// + /// The span of bytes to append. + /// Optional separator character to insert between bytes. + /// If true, uses lowercase hex characters (a-f); if false, uses uppercase (A-F). + /// + /// Each byte is converted to two hexadecimal characters. If a separator is specified, + /// it will be inserted between each pair of hex characters representing a byte. + /// For example, with separator '-': "AA-BB-CC" + /// + public void Append(ReadOnlySpan bytes, char? separator = null, bool lowercase = false) + { + var hexmap = lowercase ? lowerHexmap : upperHexmap; + + if (bytes.IsEmpty) + { + return; + } + + if (separator is { } s) + { + for (int i = 0; i < bytes.Length;) + { + var b = bytes[i]; + Append(hexmap[(int)b >> 4]); + Append(hexmap[(int)b & 0b1111]); + if (++i < bytes.Length) + { + Append(s); + } + } + } + else + { + for (var i = 0; i < bytes.Length; i++) + { + var b = bytes[i]; + Append(hexmap[(int)b >> 4]); + Append(hexmap[(int)b & 0b1111]); + } + } + } + } +} diff --git a/src/sys/ValueStringBuilder.cs b/src/sys/ValueStringBuilder.cs new file mode 100644 index 0000000000..bcbf87ddc4 --- /dev/null +++ b/src/sys/ValueStringBuilder.cs @@ -0,0 +1,529 @@ +// Based on System.Text.ValueStringBuilder - System.Console + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SIPSorcery.Sys +{ + /// + /// A ref struct that provides a low-allocation way to build strings. + /// Similar to StringBuilder but stackalloc-based for better performance. + /// + internal ref partial struct ValueStringBuilder + { + /// The array to return to the array pool, if one was rented. + private char[]? _arrayToReturnToPool; + /// The span containing the characters written so far. + private Span _chars; + /// The current position within the span. + private int _pos; + + /// + /// Initializes a new instance of ValueStringBuilder with a provided character buffer. + /// + /// The initial buffer to use for storing characters. + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } + + /// + /// Initializes a new instance of ValueStringBuilder with a specified initial capacity. + /// + /// The initial capacity of the internal buffer. + public ValueStringBuilder(int initialCapacity) + { + _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _chars = _arrayToReturnToPool; + _pos = 0; + } + + /// + /// Gets or sets the length of the current builder's content. + /// + public int Length + { + get => _pos; + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= _chars.Length); + _pos = value; + } + } + + /// + /// Gets the total capacity of the builder's buffer. + /// + public int Capacity => _chars.Length; + + /// + /// Gets a read-only span containing the builder's characters. + /// + public ReadOnlySpan Chars => _chars; + + /// + /// Ensures the builder has enough capacity to accommodate a specified total number of characters. + /// + /// The minimum capacity needed. + public void EnsureCapacity(int capacity) + { + // This is not expected to be called this with negative capacity + Debug.Assert(capacity >= 0); + + // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception. + if ((uint)capacity > (uint)_chars.Length) + { + Grow(capacity - _pos); + } + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)" + /// + /// A reference to the underlying characters. + public ref char GetPinnableReference() => ref MemoryMarshal.GetReference(_chars); + + /// + /// Get a pinnable reference to the builder. + /// + /// If , ensures that the builder has a null char after + /// A reference to the underlying characters. + public ref char GetPinnableReference(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return ref MemoryMarshal.GetReference(_chars); + } + + /// + /// Gets a reference to the character at the specified position. + /// + /// The zero-based index of the character to get. + /// A reference to the character at the specified position. + public ref char this[int index] + { + get + { + Debug.Assert(index < _pos); + return ref _chars[index]; + } + } + + /// + /// Returns the built string and disposes the builder. + /// + /// The final string. + public new string ToString() + { + var s = _chars.Slice(0, _pos).ToString(); + Dispose(); + return s; + } + + /// + /// Returns the underlying storage of the builder. + /// + public Span RawChars => _chars; + + /// + /// Returns a span around the contents of the builder. + /// + /// If , ensures that the builder has a null char after + /// A read-only span of the builder's content. + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return _chars.Slice(0, _pos); + } + + /// Returns a read-only span of the builder's content. + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); + + /// Returns a read-only span starting at the specified index. + /// The starting index. + public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); + + /// Returns a read-only span of the specified length starting at the specified index. + /// The starting index. + /// The length of the span. + public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); + + /// + /// Attempts to copy the builder's contents to a destination span. + /// + /// The destination span. + /// When this method returns, contains the number of characters that were copied. + /// if the copy was successful; otherwise, . + public bool TryCopyTo(Span destination, out int charsWritten) + { + if (_chars.Slice(0, _pos).TryCopyTo(destination)) + { + charsWritten = _pos; + Dispose(); + return true; + } + else + { + charsWritten = 0; + Dispose(); + return false; + } + } + + /// + /// Inserts a repeated character at the specified position. + /// + /// The position to insert at. + /// The character to insert. + /// The number of times to insert the character. + public void Insert(int index, char value, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + var remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + _chars.Slice(index, count).Fill(value); + _pos += count; + } + + /// + /// Inserts a string at the specified position. + /// + /// The position to insert at. + /// The string to insert. + public void Insert(int index, string? s) + { + if (s == null) + { + return; + } + + var count = s.Length; + + if (_pos > (_chars.Length - count)) + { + Grow(count); + } + + var remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + s.AsSpan().CopyTo(_chars.Slice(index)); + _pos += count; + } + + /// + /// Appends a boolean value as its string representation. + /// + /// The value to append. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(bool value) => Append(value ? "true" : "false"); + + /// + /// Appends a character to the builder. + /// + /// The character to append. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + var pos = _pos; + var chars = _chars; + if ((uint)pos < (uint)chars.Length) + { + chars[pos] = c; + _pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + /// + /// Appends a string to the builder. + /// + /// The string to append. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string? s) + { + if (s == null) + { + return; + } + + var pos = _pos; + if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. + { + _chars[pos] = s[0]; + _pos = pos + 1; + } + else + { + AppendSlow(s); + } + } + + /// + /// Slow path for appending a string when the fast path isn't applicable. + /// + private void AppendSlow(string s) + { + var pos = _pos; + if (pos > _chars.Length - s.Length) + { + Grow(s.Length); + } + + s.AsSpan().CopyTo(_chars.Slice(pos)); + _pos += s.Length; + } + + /// + /// Appends a character multiple times to the builder. + /// + /// The character to append. + /// The number of times to append the character. + public void Append(char c, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + var dst = _chars.Slice(_pos, count); + for (var i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + _pos += count; + } + + /// + /// Appends a span of characters to the builder. + /// + /// The span to append. + public void Append(scoped ReadOnlySpan value) + { + var pos = _pos; + if (pos > _chars.Length - value.Length) + { + Grow(value.Length); + } + + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + /// + /// Reserves space for a span of characters and returns a span that can be written to. + /// + /// The number of characters to reserve space for. + /// A span that can be written to. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + var origPos = _pos; + if (origPos > _chars.Length - length) + { + Grow(length); + } + + _pos = origPos + length; + return _chars.Slice(origPos, length); + } + + /// + /// Appends an value to the builder. + /// + /// The value to append. + /// An optional format string that guides the formatting, or null to use default formatting. + /// An optional object that provides culture-specific formatting services, or null to use default formatting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(int value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(int value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + /// + /// Appends an value to the builder. + /// + /// The value to append. + /// An optional format string that guides the formatting, or null to use default formatting. + /// An optional object that provides culture-specific formatting services, or null to use default formatting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(uint value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(uint value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + /// + /// Appends an value to the builder. + /// + /// The value to append. + /// An optional format string that guides the formatting, or null to use default formatting. + /// An optional object that provides culture-specific formatting services, or null to use default formatting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(ushort value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(ushort value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + /// + /// Appends an value to the builder. + /// + /// The value to append. + /// An optional format string that guides the formatting, or null to use default formatting. + /// An optional object that provides culture-specific formatting services, or null to use default formatting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(ushort? value, string? format = null, IFormatProvider? provider = null) + { + if (value is { } v) + { + AppendSpanFormattable(v, format, provider); + } + } +#else + public void Append(ushort? value, string? format = null, IFormatProvider? provider = null) + { + if (value is { } v) + { + Append(v.ToString(format, provider)); + } + } +#endif + + /// + /// Appends an value to the builder. + /// + /// The value to append. + /// An optional format string that guides the formatting, or null to use default formatting. + /// An optional object that provides culture-specific formatting services, or null to use default formatting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(long value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(long value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + /// + /// Appends an value to the builder. + /// + /// The value to append. + /// An optional format string that guides the formatting, or null to use default formatting. + /// An optional object that provides culture-specific formatting services, or null to use default formatting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(float value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(float value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + /// + /// Appends an value to the builder. + /// + /// The value to append. + /// An optional format string that guides the formatting, or null to use default formatting. + /// An optional object that provides culture-specific formatting services, or null to use default formatting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(double value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(double value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + /// + /// Appends an value to the builder. + /// + /// The value to append. + /// An optional format string that guides the formatting, or null to use default formatting. + /// An optional object that provides culture-specific formatting services, or null to use default formatting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(decimal value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(decimal value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + /// + /// Grows the buffer and appends a single character. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) + { + Grow(1); + Append(c); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int additionalCapacityBeyondPos) + { + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + + const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength + + // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try + // to double the size if possible, bounding the doubling to not go beyond the max array length. + var newCapacity = (int)Math.Max( + (uint)(_pos + additionalCapacityBeyondPos), + Math.Min((uint)_chars.Length * 2, ArrayMaxLength)); + + // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. + // This could also go negative if the actual required length wraps around. + var poolArray = ArrayPool.Shared.Rent(newCapacity); + + _chars.Slice(0, _pos).CopyTo(poolArray); + + var toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + /// + /// Disposes the builder, returning any rented array to the pool. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + var toReturn = _arrayToReturnToPool; + this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + } +} diff --git a/test/unit/net/RTCP/RTCPHeaderUnitTest.cs b/test/unit/net/RTCP/RTCPHeaderUnitTest.cs index 023dc76efc..b7d98a1a65 100644 --- a/test/unit/net/RTCP/RTCPHeaderUnitTest.cs +++ b/test/unit/net/RTCP/RTCPHeaderUnitTest.cs @@ -13,6 +13,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; using Xunit; @@ -62,7 +63,7 @@ public void RTCPHeaderRoundTripTest() logger.LogDebug("PacketType: {SrcPacketType}, {DstPacketType}", src.PacketType, dst.PacketType); logger.LogDebug("Length: {SrcLength}, {DstLength}", src.Length, dst.Length); - logger.LogDebug("Raw Header: {RawHeader}", headerBuffer.HexStr(headerBuffer.Length)); + logger.LogDebug("Raw Header: {RawHeader}", headerBuffer.AsSpan(0, headerBuffer.Length).HexStr()); Assert.True(src.Version == dst.Version, "Version was mismatched."); Assert.True(src.PaddingFlag == dst.PaddingFlag, "PaddingFlag was mismatched.");