Skip to content

Commit 2e9bc9f

Browse files
author
Paulo Morgado
committed
Enhance performance and memory management across the codebase
- Added `Microsoft.Extensions.ObjectPool` package for improved object pooling. - Introduced `Microsoft.NETFramework.ReferenceAssemblies.net462` for `net462` target framework. - Updated project warnings and added `AllowUnsafeBlocks` property for specific frameworks. - Refactored multiple `ToString` methods and other string handling to use `ValueStringBuilder`, reducing allocations and improving performance. - Updated `SIPAuthorisationDigest` and `SIPConstants` for better memory management using `AsSpan()` and `ValueStringBuilder`. - Enhanced `CRC32` class and JSON serialization in `JSONWriter` to utilize span-based operations for efficiency. - Updated unit tests to reflect changes in logging and data handling.
1 parent 1582e3d commit 2e9bc9f

29 files changed

+2227
-519
lines changed

src/SIPSorcery.csproj

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<PackageReference Include="Concentus" Version="2.2.2" />
2222
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
2323
<PackageReference Include="DnsClient" Version="1.8.0" />
24+
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.16" />
2425
<PackageReference Include="SIPSorcery.WebSocketSharp" Version="0.0.1" />
2526
<PackageReference Include="SIPSorceryMedia.Abstractions" Version="8.0.12" />
2627
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
@@ -35,15 +36,22 @@
3536
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
3637
</ItemGroup>
3738

39+
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
40+
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net462" Version="1.0.3">
41+
<PrivateAssets>all</PrivateAssets>
42+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
43+
</PackageReference>
44+
</ItemGroup>
45+
3846
<PropertyGroup>
3947
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0</TargetFrameworks>
4048
<LangVersion>12.0</LangVersion>
4149
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
42-
<NoWarn>$(NoWarn);SYSLIB0050</NoWarn>
50+
<NoWarn>$(NoWarn);SYSLIB0050;CS1591;CS1573;CS1587</NoWarn>
4351
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
52+
<WarningsNotAsErrors>$(WarningsNotAsErrors);NU1510;CS0809;CS0618;CS8632</WarningsNotAsErrors>
4453
<GenerateDocumentationFile>true</GenerateDocumentationFile>
4554
<!-- Disable warning for missing XML doc comments. -->
46-
<NoWarn>$(NoWarn);CS1591;CS1573;CS1587</NoWarn>
4755
<Authors>Aaron Clauson, Christophe Irles, Rafael Soares &amp; Contributors</Authors>
4856
<Copyright>Copyright © 2010-2025 Aaron Clauson</Copyright>
4957
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
@@ -94,6 +102,7 @@
94102
<IncludeSymbols>true</IncludeSymbols>
95103
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
96104
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
105+
<AllowUnsafeBlocks Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'netstandard2.0'">true</AllowUnsafeBlocks>
97106
</PropertyGroup>
98107

99108
</Project>

src/core/SIP/SIPAuthorisationDigest.cs

Lines changed: 111 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -256,24 +256,116 @@ public string GetDigest()
256256

257257
public override string ToString()
258258
{
259-
string authHeader = AuthHeaders.AUTH_DIGEST_KEY + " ";
260-
261-
authHeader += (Username != null && Username.Trim().Length != 0) ? AuthHeaders.AUTH_USERNAME_KEY + "=\"" + Username + "\"" : null;
262-
authHeader += (authHeader.IndexOf('=') != -1) ? "," + AuthHeaders.AUTH_REALM_KEY + "=\"" + Realm + "\"" : AuthHeaders.AUTH_REALM_KEY + "=\"" + Realm + "\"";
263-
authHeader += (Nonce != null) ? "," + AuthHeaders.AUTH_NONCE_KEY + "=\"" + Nonce + "\"" : null;
264-
authHeader += (URI != null && URI.Trim().Length != 0) ? "," + AuthHeaders.AUTH_URI_KEY + "=\"" + URI + "\"" : null;
265-
authHeader += (Response != null && Response.Length != 0) ? "," + AuthHeaders.AUTH_RESPONSE_KEY + "=\"" + Response + "\"" : null;
266-
authHeader += (Cnonce != null) ? "," + AuthHeaders.AUTH_CNONCE_KEY + "=\"" + Cnonce + "\"" : null;
267-
authHeader += (NonceCount != 0) ? "," + AuthHeaders.AUTH_NONCECOUNT_KEY + "=" + GetPaddedNonceCount(NonceCount) : null;
268-
authHeader += (Qop != null) ? "," + AuthHeaders.AUTH_QOP_KEY + "=" + Qop : null;
269-
authHeader += (Opaque != null) ? "," + AuthHeaders.AUTH_OPAQUE_KEY + "=\"" + Opaque + "\"" : null;
270-
271-
string algorithmID = (DigestAlgorithm == DigestAlgorithmsEnum.SHA256) ? SHA256_ALGORITHM_ID : DigestAlgorithm.ToString();
272-
authHeader += (Response != null) ? "," + AuthHeaders.AUTH_ALGORITHM_KEY + "=" + algorithmID : null;
273-
274-
return authHeader;
259+
var builder = new ValueStringBuilder();
260+
261+
try
262+
{
263+
ToString(ref builder);
264+
265+
return builder.ToString();
266+
}
267+
finally
268+
{
269+
builder.Dispose();
270+
}
271+
}
272+
273+
internal void ToString(ref ValueStringBuilder builder)
274+
{
275+
builder.Append(AuthHeaders.AUTH_DIGEST_KEY);
276+
builder.Append(' ');
277+
278+
bool hasUsername = !string.IsNullOrWhiteSpace(Username);
279+
if (hasUsername)
280+
{
281+
builder.Append(AuthHeaders.AUTH_USERNAME_KEY);
282+
builder.Append("=\"");
283+
builder.Append(Username);
284+
builder.Append('"');
285+
}
286+
287+
builder.Append(hasUsername ? ',' : '\0');
288+
builder.Append(AuthHeaders.AUTH_REALM_KEY);
289+
builder.Append("=\"");
290+
builder.Append(Realm);
291+
builder.Append('"');
292+
293+
if (Nonce != null)
294+
{
295+
builder.Append(',');
296+
builder.Append(AuthHeaders.AUTH_NONCE_KEY);
297+
builder.Append("=\"");
298+
builder.Append(Nonce);
299+
builder.Append('"');
300+
}
301+
302+
if (!string.IsNullOrWhiteSpace(URI))
303+
{
304+
builder.Append(',');
305+
builder.Append(AuthHeaders.AUTH_URI_KEY);
306+
builder.Append("=\"");
307+
builder.Append(URI);
308+
builder.Append('"');
309+
}
310+
311+
if (!string.IsNullOrEmpty(Response))
312+
{
313+
builder.Append(',');
314+
builder.Append(AuthHeaders.AUTH_RESPONSE_KEY);
315+
builder.Append("=\"");
316+
builder.Append(Response);
317+
builder.Append('"');
318+
}
319+
320+
if (Cnonce != null)
321+
{
322+
builder.Append(',');
323+
builder.Append(AuthHeaders.AUTH_CNONCE_KEY);
324+
builder.Append("=\"");
325+
builder.Append(Cnonce);
326+
builder.Append('"');
327+
}
328+
329+
if (NonceCount != 0)
330+
{
331+
builder.Append(',');
332+
builder.Append(AuthHeaders.AUTH_NONCECOUNT_KEY);
333+
builder.Append('=');
334+
builder.Append(GetPaddedNonceCount(NonceCount));
335+
}
336+
337+
if (Qop != null)
338+
{
339+
builder.Append(',');
340+
builder.Append(AuthHeaders.AUTH_QOP_KEY);
341+
builder.Append('=');
342+
builder.Append(Qop);
343+
}
344+
345+
if (Opaque != null)
346+
{
347+
builder.Append(',');
348+
builder.Append(AuthHeaders.AUTH_OPAQUE_KEY);
349+
builder.Append("=\"");
350+
builder.Append(Opaque);
351+
builder.Append('"');
352+
}
353+
354+
if (Response != null)
355+
{
356+
builder.Append(',');
357+
builder.Append(AuthHeaders.AUTH_ALGORITHM_KEY);
358+
builder.Append('=');
359+
360+
string algorithmID = (DigestAlgorithm == DigestAlgorithmsEnum.SHA256)
361+
? SHA256_ALGORITHM_ID
362+
: DigestAlgorithm.ToString();
363+
364+
builder.Append(algorithmID);
365+
}
275366
}
276367

368+
277369
public SIPAuthorisationDigest CopyOf()
278370
{
279371
var copy = new SIPAuthorisationDigest(AuthorisationType, Realm, Username, Password, URI, Nonce, RequestType, DigestAlgorithm);
@@ -385,21 +477,21 @@ public static string GetHashHex(DigestAlgorithmsEnum hashAlg, string val)
385477
case DigestAlgorithmsEnum.SHA256:
386478
using (var hash = new SHA256CryptoServiceProvider())
387479
{
388-
return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr().ToLower();
480+
return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).AsSpan().HexStr(lowercase: true);
389481
}
390482
// This is commented because RFC8760 does not have an SHA-512 option. Instead it's HSA-512-sess which
391483
// means the SIP request body needs to be included in the digest as well. Including the body will require
392484
// some additional changes that can be done at a later date.
393485
//case DigestAlgorithmsEnum.SHA512:
394486
// using (var hash = new SHA512CryptoServiceProvider())
395487
// {
396-
// return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr().ToLower();
488+
// return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr(lowercase: false);
397489
// }
398490
case DigestAlgorithmsEnum.MD5:
399491
default:
400492
using (var hash = new MD5CryptoServiceProvider())
401493
{
402-
return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr().ToLower();
494+
return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).AsSpan().HexStr(lowercase: true);
403495
}
404496
}
405497
#pragma warning restore SYSLIB0021

src/core/SIP/SIPConstants.cs

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -541,17 +541,68 @@ public static string SIPURIUserUnescape(string escapedString)
541541

542542
public static string SIPURIParameterEscape(string unescapedString)
543543
{
544-
string result = unescapedString;
545-
if (!result.IsNullOrBlank())
544+
// Characters that need escaping
545+
ReadOnlySpan<char> specialChars = stackalloc char[] { ';', '?', '@', '=', ',', ' ' };
546+
547+
// Early exit if no special characters are found
548+
var unescapedSpan = unescapedString.AsSpan();
549+
int nextIndex = unescapedSpan.IndexOfAny(specialChars);
550+
if (nextIndex == -1)
546551
{
547-
result = result.Replace(";", "%3B");
548-
result = result.Replace("?", "%3F");
549-
result = result.Replace("@", "%40");
550-
result = result.Replace("=", "%3D");
551-
result = result.Replace(",", "%2C");
552-
result = result.Replace(" ", "%20");
552+
// No escaping needed
553+
return unescapedString;
554+
}
555+
556+
var builder = new ValueStringBuilder();
557+
558+
try
559+
{
560+
SIPURIParameterEscape(ref builder, unescapedSpan);
561+
562+
return builder.ToString();
563+
}
564+
finally
565+
{
566+
builder.Dispose();
567+
}
568+
}
569+
570+
internal static void SIPURIParameterEscape(ref ValueStringBuilder builder, ReadOnlySpan<char> unescapedSpan)
571+
{
572+
// Characters that need escaping
573+
ReadOnlySpan<char> specialChars = stackalloc char[] { ';', '?', '@', '=', ',', ' ' };
574+
575+
var currentIndex = 0;
576+
var nextIndex = unescapedSpan.IndexOfAny(specialChars);
577+
while (nextIndex != -1)
578+
{
579+
// Append everything before the special character
580+
builder.Append(unescapedSpan.Slice(currentIndex, nextIndex - currentIndex));
581+
582+
// Escape the special character
583+
switch (unescapedSpan[nextIndex])
584+
{
585+
case ';': builder.Append("%3B"); break;
586+
case '?': builder.Append("%3F"); break;
587+
case '@': builder.Append("%40"); break;
588+
case '=': builder.Append("%3D"); break;
589+
case ',': builder.Append("%2C"); break;
590+
case ' ': builder.Append("%20"); break;
591+
}
592+
593+
currentIndex = nextIndex + 1;
594+
nextIndex = unescapedSpan.Slice(currentIndex).IndexOfAny(specialChars);
595+
if (nextIndex != -1)
596+
{
597+
nextIndex += currentIndex; // Adjust relative index to absolute
598+
}
599+
}
600+
601+
// Append the remaining part
602+
if (currentIndex < unescapedSpan.Length)
603+
{
604+
builder.Append(unescapedSpan.Slice(currentIndex));
553605
}
554-
return result;
555606
}
556607

557608
public static string SIPURIParameterUnescape(string escapedString)

0 commit comments

Comments
 (0)