diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/PacketBuffer.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/PacketBuffer.cs
new file mode 100644
index 0000000000..079c2933a5
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/PacketBuffer.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+
+#nullable enable
+
+namespace Microsoft.Data.Common;
+
+///
+/// One buffer, which may contain one unparsed packet from a single destination.
+///
+internal sealed class PacketBuffer : ReadOnlySequenceSegment
+{
+ public PacketBuffer(ReadOnlyMemory buffer, PacketBuffer? previous)
+ {
+ Memory = buffer;
+
+ if (previous is not null)
+ {
+ previous.Next = this;
+ RunningIndex = previous.RunningIndex + previous.Memory.Length;
+ }
+ else
+ {
+ RunningIndex = 0;
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ReadOnlySequenceUtilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ReadOnlySequenceUtilities.cs
new file mode 100644
index 0000000000..792ca6ddfc
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ReadOnlySequenceUtilities.cs
@@ -0,0 +1,92 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+using System.Buffers.Binary;
+
+namespace Microsoft.Data.Common;
+
+internal static class ReadOnlySequenceUtilities
+{
+ ///
+ /// Reads the next byte from the sequence, advancing its position by one byte.
+ ///
+ /// The sequence to read and to advance from.
+ /// The first span in the sequence. Reassigned if the next byte can only be read from the next span.
+ /// Current position in the sequence. Advanced by one byte following a successful read.
+ /// The value read from .
+ /// true if was long enough to retrieve the next byte, false otherwise.
+ public static bool ReadByte(this ref ReadOnlySequence sequence, ref ReadOnlySpan currSpan, ref long currPos, out byte value)
+ {
+ if (sequence.Length < sizeof(byte))
+ {
+ value = default;
+ return false;
+ }
+
+ currPos += sizeof(byte);
+ if (currSpan.Length >= sizeof(byte))
+ {
+ value = currSpan[0];
+
+ sequence = sequence.Slice(sizeof(byte));
+ currSpan = currSpan.Slice(sizeof(byte));
+
+ return true;
+ }
+ else
+ {
+ Span buffer = stackalloc byte[sizeof(byte)];
+
+ sequence.Slice(0, sizeof(byte)).CopyTo(buffer);
+ value = buffer[0];
+
+ sequence = sequence.Slice(sizeof(byte));
+ currSpan = sequence.First.Span;
+
+ return true;
+ }
+ }
+
+ ///
+ /// Reads the next two bytes from the sequence as a , advancing its position by two bytes.
+ ///
+ /// The sequence to read and to advance from.
+ /// The first span in the sequence. Reassigned if the next two bytes can only be read from the next span.
+ /// Current position in the sequence. Advanced by two bytes following a successful read.
+ /// The value read from
+ /// true if was long enough to retrieve the next two bytes, false otherwise.
+ public static bool ReadLittleEndian(this ref ReadOnlySequence sequence, ref ReadOnlySpan currSpan, ref long currPos, out ushort value)
+ {
+ if (sequence.Length < sizeof(ushort))
+ {
+ value = default;
+ return false;
+ }
+
+ currPos += sizeof(ushort);
+ if (currSpan.Length >= sizeof(ushort))
+ {
+ value = BinaryPrimitives.ReadUInt16LittleEndian(currSpan);
+
+ sequence = sequence.Slice(sizeof(ushort));
+ currSpan = currSpan.Slice(sizeof(ushort));
+
+ return true;
+ }
+ else
+ {
+ Span buffer = stackalloc byte[sizeof(ushort)];
+
+ sequence.Slice(0, sizeof(ushort)).CopyTo(buffer);
+ value = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
+
+ sequence = sequence.Slice(sizeof(ushort));
+ currSpan = sequence.First.Span;
+
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/src/System/Diagnostics/CodeAnalysis.netfx.cs b/src/Microsoft.Data.SqlClient/src/System/Diagnostics/CodeAnalysis.netfx.cs
index 1bf90ad001..4ac00aae59 100644
--- a/src/Microsoft.Data.SqlClient/src/System/Diagnostics/CodeAnalysis.netfx.cs
+++ b/src/Microsoft.Data.SqlClient/src/System/Diagnostics/CodeAnalysis.netfx.cs
@@ -15,6 +15,14 @@ namespace System.Diagnostics.CodeAnalysis
internal sealed class NotNullAttribute : Attribute
{
}
+
+ [AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
+ internal sealed class NotNullWhenAttribute : Attribute
+ {
+ public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
+
+ public bool ReturnValue { get; }
+ }
}
#endif
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseProcessorTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseProcessorTest.cs
new file mode 100644
index 0000000000..fe56f8c5d5
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseProcessorTest.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers;
+using Xunit;
+
+namespace Microsoft.Data.Sql.UnitTests;
+
+public class DacResponseProcessorTest
+{
+ [Theory(Skip = "Implementation in progress, see GH #3700")]
+ [MemberData(nameof(SsrpPacketTestData.EmptyPacketBuffer), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)]
+ public void Process_EmptyBuffer_ReturnsFalse(ReadOnlySequence packetBuffers)
+ {
+ _ = packetBuffers;
+ }
+
+ [Theory(Skip = "Implementation in progress, see GH #3700")]
+ [MemberData(nameof(SsrpPacketTestData.InvalidSvrRespDacPackets), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)]
+ public void Process_InvalidDacResponse_ReturnsFalse(ReadOnlySequence packetBuffers)
+ {
+ _ = packetBuffers;
+ }
+
+ [Theory(Skip = "Implementation in progress, see GH #3700")]
+ [MemberData(nameof(SsrpPacketTestData.ValidSvrRespDacPacketBuffer), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)]
+ public void Process_ValidDacResponse_ReturnsData(ReadOnlySequence packetBuffers, int expectedDacPort)
+ {
+ _ = packetBuffers;
+ _ = expectedDacPort;
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SqlDataSourceResponseProcessorTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SqlDataSourceResponseProcessorTest.cs
new file mode 100644
index 0000000000..695a8a5cbb
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SqlDataSourceResponseProcessorTest.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers;
+using Xunit;
+
+namespace Microsoft.Data.Sql.UnitTests;
+
+public class SqlDataSourceResponseProcessorTest
+{
+ [Theory(Skip = "Implementation in progress, see GH #3700")]
+ [MemberData(nameof(SsrpPacketTestData.EmptyPacketBuffer), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)]
+ public void Process_EmptyBuffer_ReturnsFalse(ReadOnlySequence packetBuffers)
+ {
+ _ = packetBuffers;
+ }
+
+ [Theory(Skip = "Implementation in progress, see GH #3700")]
+ [MemberData(nameof(SsrpPacketTestData.InvalidSvrRespPackets), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)]
+ public void Process_InvalidSqlDataSourceResponse_ReturnsFalse(ReadOnlySequence packetBuffers)
+ {
+ _ = packetBuffers;
+ }
+
+ [Theory(Skip = "Implementation in progress, see GH #3700")]
+ [MemberData(nameof(SsrpPacketTestData.InvalidRespDataPackets), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)]
+ public void Process_InvalidSqlDataSourceResponse_RespData_ReturnsFalse(ReadOnlySequence packetBuffers)
+ {
+ _ = packetBuffers;
+ }
+
+ [Theory(Skip = "Implementation in progress, see GH #3700")]
+ [MemberData(nameof(SsrpPacketTestData.InvalidTcpInfoPackets), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)]
+ public void Process_InvalidSqlDataSourceResponse_TcpInfo_ReturnsFalse(ReadOnlySequence packetBuffers)
+ {
+ _ = packetBuffers;
+ }
+
+ [Theory(Skip = "Implementation in progress, see GH #3700")]
+ [MemberData(nameof(SsrpPacketTestData.InvalidClntUcastInstSvrRespPackets), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)]
+ public void Process_InvalidSqlDataSourceResponseToClntUcastInst_ReturnsFalse(ReadOnlySequence packetBuffers)
+ {
+ _ = packetBuffers;
+ }
+
+ [Theory(Skip = "Implementation in progress, see GH #3700")]
+ [MemberData(nameof(SsrpPacketTestData.ValidSvrRespPacketBuffer), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)]
+ public void Process_ValidSqlDataSourceResponse_ReturnsData(ReadOnlySequence packetBuffers, string expectedVersion, int expectedTcpPort, string? expectedPipeName)
+ {
+ _ = packetBuffers;
+ _ = expectedVersion;
+ _ = expectedTcpPort;
+ _ = expectedPipeName;
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs
new file mode 100644
index 0000000000..9729c33949
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs
@@ -0,0 +1,738 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Data.Common;
+using System;
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+
+namespace Microsoft.Data.Sql.UnitTests;
+
+///
+/// Test cases used to verify the successful processing of valid SSRP responses and the silent
+/// discarding of invalid SSRP responses.
+///
+internal static class SsrpPacketTestData
+{
+ private const byte ValidSvrRespHeader = 0x05;
+ private const byte ValidRespDataDacProtocolVersion = 0x01;
+ private const byte ValidRespDataDacResponseSize = 0x06;
+
+ private const string ValidServerName = "srv1";
+ private const string ValidInstanceName = "MSSQLSERVER";
+ private const string ValidServerVersion = "14.0.0.0";
+ private const int ValidTcpPort1 = 1433;
+ private const int ValidTcpPort2 = 1434;
+ private const int ValidTcpPort3 = 1435;
+ private const int ValidTcpPort4 = 1436;
+ private const int ValidTcpPort5 = 1437;
+
+ ///
+ /// One empty packet buffer, which should be successfully processed and contain zero responses.
+ ///
+ ///
+ ///
+ public static TheoryData> EmptyPacketBuffer =>
+ new(GeneratePacketBuffers([]));
+
+ ///
+ /// Various combinations of packet buffers containing normal SVR_RESP responses, all of which
+ /// should be successfully processed.
+ ///
+ ///
+ public static TheoryData, string, int, string?> ValidSvrRespPacketBuffer
+ {
+ get
+ {
+ const string PipeName = $@"\\{ValidServerName}\pipe\SampléPipeName";
+
+ string complexProtocolParameters = CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort1}",
+ npInfo: $"np;{PipeName}",
+ viaInfo: $"via;{ValidServerName} 1:1433",
+ rpcInfo: $"rpc;{ValidServerName}",
+ spxInfo: $"spx;{ValidInstanceName}",
+ adspInfo: "adsp;SQL2000",
+ bvInfo: "bv;item;group;item;group;org");
+
+ byte[] complexValidPacket = FormatSvrRespMessage(ValidSvrRespHeader,
+ respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion, complexProtocolParameters));
+ byte[] validPacket1 = FormatSvrRespMessage(ValidSvrRespHeader,
+ respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion, CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort1}")));
+ byte[] validPacket2 = FormatSvrRespMessage(ValidSvrRespHeader,
+ respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion, CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort2}")));
+ byte[] validPacket3 = FormatSvrRespMessage(ValidSvrRespHeader,
+ respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion, CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort3}")));
+ byte[] validPacket4 = FormatSvrRespMessage(ValidSvrRespHeader,
+ respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion, CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort4}")));
+ byte[] invalidPacket1 = FormatSvrRespMessage(ValidSvrRespHeader,
+ respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, "v14", CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort1}")));
+
+ return new()
+ {
+ {
+ // One buffer, one response
+ GeneratePacketBuffers(complexValidPacket),
+ ValidServerVersion,
+ ValidTcpPort1,
+ PipeName
+ },
+ {
+ // One response, split into four buffers in the middle of a string
+ GeneratePacketBuffers(
+ complexValidPacket.AsSpan(0, 14).ToArray(),
+ complexValidPacket.AsSpan(14, 22).ToArray(),
+ complexValidPacket.AsSpan(36, 71).ToArray(),
+ // Position 107 is the second byte of the é character when encoded to UTF8.
+ complexValidPacket.AsSpan(107).ToArray()),
+ ValidServerVersion,
+ ValidTcpPort1,
+ PipeName
+ },
+ {
+ // Four responses, each with different methods
+ GeneratePacketBuffers(
+ validPacket1,
+ validPacket2,
+ validPacket3,
+ validPacket4),
+ ValidServerVersion,
+ ValidTcpPort4,
+ null
+ },
+ {
+ // Five responses, with response three invalid
+ GeneratePacketBuffers(
+ complexValidPacket,
+ validPacket2,
+ invalidPacket1,
+ validPacket3,
+ validPacket4),
+ ValidServerVersion,
+ ValidTcpPort4,
+ null
+ }
+ };
+ }
+ }
+
+ ///
+ /// Various combinations of packet buffers containing SVR_RESP (DAC) responses, all of which
+ /// should be successfully processed.
+ ///
+ ///
+ public static TheoryData, int> ValidSvrRespDacPacketBuffer
+ {
+ get
+ {
+ byte[] validPacket1 = FormatSvrRespMessage(ValidSvrRespHeader,
+ ValidRespDataDacResponseSize,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort2));
+ byte[] validPacket2 = FormatSvrRespMessage(ValidSvrRespHeader,
+ ValidRespDataDacResponseSize,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort3));
+ byte[] validPacket3 = FormatSvrRespMessage(ValidSvrRespHeader,
+ ValidRespDataDacResponseSize,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort4));
+ byte[] validPacket4 = FormatSvrRespMessage(ValidSvrRespHeader,
+ ValidRespDataDacResponseSize,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort5));
+ byte[] invalidPacket1 = FormatSvrRespMessage(ValidSvrRespHeader,
+ serializedResponseSize: 0x03,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort2));
+
+ return new()
+ {
+ {
+ // One buffer, one response
+ GeneratePacketBuffers(validPacket1),
+ ValidTcpPort2
+ },
+ {
+ // One response, split into three buffers.
+ // Buffer 1: the header byte
+ // Buffer 2: both bytes of RESP_SIZE and the first byte of RESP_DATA (protocol version).
+ // Buffer 3: remainder.
+ GeneratePacketBuffers(validPacket1.AsSpan(0, 1).ToArray(),
+ validPacket1.AsSpan(1, 3).ToArray(),
+ validPacket1.AsSpan(4).ToArray()),
+ ValidTcpPort2
+ },
+ {
+ // One response, split into three buffers.
+ // Buffer 1: the header and first byte of RESP_SIZE.
+ // Buffer 2: the second byte of RESP_SIZE and the first byte of RESP_DATA (protocol version).
+ // Buffer 3: remainder.
+ GeneratePacketBuffers(validPacket1.AsSpan(0, 2).ToArray(),
+ validPacket1.AsSpan(2, 2).ToArray(),
+ validPacket1.AsSpan(4).ToArray()),
+ ValidTcpPort2
+ },
+ {
+ // Two responses with trailing data
+ GeneratePacketBuffers(validPacket1.AsSpan(0, 2).ToArray(),
+ validPacket1.AsSpan(2, 2).ToArray(),
+ [..validPacket1.AsSpan(4).ToArray(), 0x05],
+ validPacket2.AsSpan(0, 2).ToArray(),
+ validPacket2.AsSpan(2).ToArray(),
+ [0x05]),
+ ValidTcpPort3
+ },
+ {
+ // Four responses, each with different DAC ports
+ GeneratePacketBuffers(validPacket1,
+ validPacket2,
+ validPacket3,
+ validPacket4),
+ ValidTcpPort5
+ },
+ {
+ // Five responses, with response three invalid
+ GeneratePacketBuffers(validPacket1,
+ validPacket2,
+ invalidPacket1,
+ validPacket3,
+ validPacket4),
+ ValidTcpPort5
+ },
+ {
+ // Four responses, with three extraneous 0x05 bytes between responses 2 and 3
+ GeneratePacketBuffers(validPacket1,
+ [..validPacket2, 0x05],
+ [0x05],
+ [0x05, ..validPacket3],
+ validPacket4),
+ ValidTcpPort5
+ }
+ };
+ }
+ }
+
+ ///
+ /// Packet buffers containing nothing but invalid SVR_RESP (DAC) responses.
+ ///
+ ///
+ public static TheoryData> InvalidSvrRespDacPackets =>
+ [
+ // Invalid header byte
+ GeneratePacketBuffers(FormatSvrRespMessage(header: 0x00,
+ ValidRespDataDacResponseSize,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort2))),
+
+ // Invalid size
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ serializedResponseSize: 0x09,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort2))),
+
+ // Invalid protocol version
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ ValidRespDataDacResponseSize,
+ CreateRespData(protocolVersion: 0x02, ValidTcpPort2))),
+
+ // Invalid port
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ ValidRespDataDacResponseSize,
+ CreateRespData(ValidRespDataDacProtocolVersion, 0)))
+ ];
+
+ ///
+ /// Packets containing an SVR_RESP response which is a valid response to a CLNT_[B|U]CAST_EX message
+ /// but not to a CLNT_UCAST_INST message.
+ ///
+ ///
+ public static TheoryData> InvalidClntUcastInstSvrRespPackets
+ {
+ get
+ {
+ // The RESP_DATA section of the response to a CLNT_UCAST_INST message must be shorter than 1024 bytes.
+ byte[] longPacket = FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort1}", npInfo: @"np;" + new string('a', 1025))));
+
+ return [GeneratePacketBuffers(longPacket)];
+ }
+ }
+
+ ///
+ /// Packet buffers containing an SSRP message which is failing due to invalid data
+ /// in the top-level SVR_RESP message fields.
+ ///
+ ///
+ public static TheoryData> InvalidSvrRespPackets =>
+ [
+ // Invalid SVR_RESP header field value
+ GeneratePacketBuffers(FormatSvrRespMessage(header: 0x04,
+ ValidRespDataDacResponseSize,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort2))),
+
+ // RESP_SIZE too small (DAC response)
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ serializedResponseSize: 0x05,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort2))),
+
+ // RESP_SIZE too large (DAC response)
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ serializedResponseSize : 0x07,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort2))),
+
+ // RESP_SIZE zero length (DAC response)
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ serializedResponseSize: 0x00,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort2))),
+
+ // RESP_SIZE far beyond reasonable limits (normal response)
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ serializedResponseSize: ushort.MaxValue,
+ CreateRespData(ValidRespDataDacProtocolVersion, ValidTcpPort2))),
+
+ // RESP_SIZE larger than the buffer (normal response)
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ serializedResponseSize: 72,
+ CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion))),
+
+ // RESP_SIZE zero length (normal response)
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ serializedResponseSize: 0x00,
+ CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion))),
+
+ // RESP_SIZE far beyond reasonable limits (normal response)
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ serializedResponseSize: ushort.MaxValue,
+ CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion)))
+ ];
+
+ ///
+ /// Packet buffers containing an SSRP message with valid top-level SVR_RESP message
+ /// fields but invalid components of the child RESP_DATA structure.
+ ///
+ ///
+ public static TheoryData> InvalidRespDataPackets
+ {
+ get
+ {
+ const string InvalidServerName = "sr\u0008v\u00001";
+ string validTcpInfo = CreateProtocolParameters($"tcp;{ValidTcpPort1}");
+
+ return [
+ // All keys lowercase
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo,
+ lowercaseKey: true))),
+
+ // Keys shuffled
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo,
+ shuffleKeys: true))),
+
+ // Does not start with "ServerName" string
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo,
+ omitServerName: true))),
+
+ // Server name longer than 255 bytes
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(serverName: new string('a', 256),
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo))),
+
+ // Server name contains invalid characters
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(serverName: InvalidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo))),
+
+ // Missing semicolons between keys and values
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo,
+ omitKeyValueSeparators: true))),
+
+ // Missing terminating pair of semicolons
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo,
+ omitTrailingSemicolons: true))),
+
+ // Missing "InstanceName"
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo,
+ omitInstanceName: true))),
+
+ // Duplicate "InstanceName" key with different value
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ protocolParameters: "InstanceName;DUPLICATE"))),
+
+ // Instance name longer than 255 bytes
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ instanceName: new string('a', 256),
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo))),
+
+ // Missing "IsClustered"
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo,
+ omitIsClustered: true))),
+
+ // Invalid IsClustered value - omit the IsClustered;Yes pair and supply an invalid one.
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ protocolParameters: "IsClustered;INVALID;" + validTcpInfo,
+ omitIsClustered: true))),
+
+ // Duplicate "IsClustered" key with different value
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ protocolParameters: "IsClustered;DUPLICATE"))),
+
+ // Missing "Version"
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ validTcpInfo,
+ omitVersion: true))),
+
+ // Empty version string
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ version: string.Empty,
+ validTcpInfo))),
+
+ // Version string longer than 16 bytes
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ version: "65535.65535.65.53",
+ validTcpInfo))),
+
+ // Version string not in the correct format: 1*[0-9"."]
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ version: "v14",
+ validTcpInfo))),
+
+ // Duplicate "Version" key with different value
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ protocolParameters: "Version;DUPLICATE"))),
+
+ // Protocol components listed twice
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort2}", otherParameters: $"tcp;{ValidTcpPort3}")))),
+
+ // Invalid protocol components appear
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort2}", otherParameters: "invalid_protocol;value")))),
+
+ // Empty protocol components appear
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ CreateProtocolParameters(otherParameters: ";value")))),
+
+ // Invalid PROTOCOLVERSION field value
+ GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader,
+ ValidRespDataDacResponseSize,
+ CreateRespData(protocolVersion: 0x02, ValidTcpPort2)))
+ ];
+ }
+ }
+
+ ///
+ /// Packet buffers containing an SSRP message with valid top-level SVR_RESP message
+ /// fields, a valid RESP_DATA child structure but an invalid TCP_INFO structure.
+ ///
+ ///
+ public static TheoryData> InvalidTcpInfoPackets
+ {
+ get
+ {
+ return [
+ // Port is absent
+ CreateSvrRespMessage("tcp"),
+
+ // Port is zero-length
+ CreateSvrRespMessage("tcp;"),
+
+ // Port is non-numeric
+ CreateSvrRespMessage("tcp;one"),
+
+ // Port is > ushort.MaxValue
+ CreateSvrRespMessage("tcp;65536"),
+
+ // Port is < 0
+ CreateSvrRespMessage("tcp;-1")
+ ];
+
+ static ReadOnlySequence CreateSvrRespMessage(string tcpInfo) =>
+ GeneratePacketBuffers(
+ FormatSvrRespMessage(ValidSvrRespHeader,
+ CreateRespData(ValidServerName,
+ ValidInstanceName,
+ isClustered: true,
+ ValidServerVersion,
+ CreateProtocolParameters(tcpInfo))));
+ }
+ }
+
+ private static ReadOnlySequence GeneratePacketBuffers(params byte[][] packetBuffers)
+ {
+ if (packetBuffers.Length == 0)
+ {
+ return ReadOnlySequence.Empty;
+ }
+
+ PacketBuffer first = new(packetBuffers[0], null);
+ PacketBuffer curr = first;
+ PacketBuffer last;
+
+ for (int i = 1; i < packetBuffers.Length; i++)
+ {
+ curr = new(packetBuffers[i], curr);
+ }
+ last = curr;
+
+ return new ReadOnlySequence(first, 0, last, last.Memory.Length);
+ }
+
+ ///
+ /// Generates an SVR_RESP message with a valid length.
+ ///
+ /// The SVR_RESP header value. Expected to be 0x05.
+ /// The serialized RESP_DATA section.
+ ///
+ /// A byte representation of one SVR_RESP message.
+ private static byte[] FormatSvrRespMessage(byte header, ReadOnlySpan respData) =>
+ FormatSvrRespMessage(header, (ushort)respData.Length, respData);
+
+ ///
+ /// Generates an SVR_RESP message with specific characteristics.
+ ///
+ /// The SVR_RESP header value. Expected to be 0x05.
+ /// The RESP_SIZE field to be serialized to the header.
+ /// The serialized RESP_DATA section.
+ /// If specified, the number of bytes to actually write.
+ /// A byte representation of one SVR_RESP message.
+ ///
+ ///
+ private static byte[] FormatSvrRespMessage(byte header,
+ ushort serializedResponseSize,
+ ReadOnlySpan respData,
+ int? realResponseSize = null)
+ {
+ byte[] realRespData = realResponseSize is null
+ ? new byte[sizeof(byte) + sizeof(ushort) + respData.Length]
+ : new byte[realResponseSize.Value];
+
+ // Pad any free space after RESP_DATA with 0x05
+ if (realRespData.Length > sizeof(byte) + sizeof(ushort) + respData.Length)
+ {
+ realRespData.AsSpan(sizeof(byte) + sizeof(ushort) + respData.Length).Fill(0x05);
+ }
+
+ // Write RESP_DATA
+ if (realRespData.Length > sizeof(byte) + sizeof(ushort))
+ {
+ int bytesToCopy = Math.Min(respData.Length, realRespData.Length - sizeof(byte) - sizeof(ushort));
+
+ respData.Slice(0, bytesToCopy).CopyTo(realRespData.AsSpan(sizeof(byte) + sizeof(ushort)));
+ }
+
+ // Write RESP_SIZE
+ if (realRespData.Length > sizeof(byte))
+ {
+ Span responseSizeBytes = stackalloc byte[sizeof(ushort)];
+ int bytesToCopy = Math.Min(responseSizeBytes.Length, realRespData.Length - sizeof(byte));
+
+ BinaryPrimitives.WriteUInt16LittleEndian(responseSizeBytes, serializedResponseSize);
+ responseSizeBytes.Slice(0, bytesToCopy).CopyTo(realRespData.AsSpan(sizeof(byte)));
+ }
+
+ // Write SVR_RESP
+ if (realRespData.Length > 0)
+ {
+ realRespData[0] = header;
+ }
+
+ return realRespData;
+ }
+
+ ///
+ /// Creates a new RESP_DATA section of an SVR_RESP message for the DAC request with a specified protocol version and TCP port number.
+ ///
+ /// Protocol version. Expected to be 0x01.
+ /// TCP port number of the DAC.
+ /// A byte representation of a RESP_DATA section.
+ ///
+ private static byte[] CreateRespData(byte protocolVersion, ushort dacPort)
+ {
+ byte[] data = new byte[sizeof(byte) + sizeof(ushort)];
+
+ data[0] = protocolVersion;
+ BinaryPrimitives.WriteUInt16LittleEndian(data.AsSpan(1), dacPort);
+ return data;
+ }
+
+ ///
+ /// Creates a RESP_DATA section of an SVR_RESP message with specific characteristics.
+ ///
+ /// ServerName parameter value.
+ /// InstanceName parameter value.
+ /// IsClustered parameter value.
+ /// Version parameter value.
+ /// If specified, the protocol parameters. Generated by .
+ /// If true, the ServerName, InstanceName, IsClustered and Version keys will be written in lowercase.
+ /// If true, the return value will not include the trailing ;;.
+ /// If true, no separators between the keys and values will be written.
+ /// If true, the mandatory ServerName parameter value will not be written.
+ /// If true, the mandatory InstanceName parameter value will not be written.
+ /// If true, the mandatory IsClustered parameter value will not be written.
+ /// If true, the mandatory Version parameter value will not be written.
+ /// If true, the key/value pairs will be written in a non-sequential order.
+ /// A byte representation of a RESP_DATA section.
+ ///
+ private static byte[] CreateRespData(string serverName,
+ string instanceName,
+ bool isClustered,
+ string version,
+ string? protocolParameters = null,
+ bool lowercaseKey = false,
+ bool omitTrailingSemicolons = false,
+ bool omitKeyValueSeparators = false,
+ bool omitServerName = false,
+ bool omitInstanceName = false,
+ bool omitIsClustered = false,
+ bool omitVersion = false,
+ bool shuffleKeys = false)
+ {
+ string serverNameKey = GenerateKeyValuePair("ServerName", serverName, omitServerName);
+ string instanceNameKey = GenerateKeyValuePair("InstanceName", instanceName, omitInstanceName);
+ string isClusteredKey = GenerateKeyValuePair("IsClustered", isClustered ? "Yes" : "No", omitIsClustered);
+ string versionKey = GenerateKeyValuePair("Version", version, omitVersion);
+ string[] components =
+ shuffleKeys
+ ? [protocolParameters ?? string.Empty, versionKey, isClusteredKey, instanceNameKey, serverNameKey]
+ : [serverNameKey, instanceNameKey, isClusteredKey, versionKey, protocolParameters ?? string.Empty];
+ string outputString = string.Join(";", components)
+ + (omitTrailingSemicolons ? string.Empty : ";;");
+
+ return Encoding.UTF8.GetBytes(outputString);
+
+ string GenerateKeyValuePair(string key, string value, bool omitKey)
+ {
+ if (omitKey)
+ {
+ return string.Empty;
+ }
+
+ if (lowercaseKey)
+ {
+ key = key.ToLower();
+ }
+
+ return key + (omitKeyValueSeparators ? string.Empty : ";") + value;
+ }
+ }
+
+ ///
+ /// Creates the protocol parameters for a RESP_DATA section.
+ ///
+ /// If non-null, the TCP_INFO data.
+ /// If non-null, the NP_INFO data.
+ /// If non-null, the VIA_INFO data.
+ /// If non-null, the RPC_INFO data
+ /// If non-null, the SPX_INFO data.
+ /// If non-null, the ADSP_INFO data.
+ /// If non-null, the BV_INFO data.
+ /// Any additional protocol parameters to include.
+ /// The collated protocol parameters for a RESP_DATA section.
+ private static string CreateProtocolParameters(string? tcpInfo = null,
+ string? npInfo = null,
+ string? viaInfo = null,
+ string? rpcInfo = null,
+ string? spxInfo = null,
+ string? adspInfo = null,
+ string? bvInfo = null,
+ string? otherParameters = null)
+ {
+ List protocolParameters = [];
+ ReadOnlySpan allParams = [tcpInfo, npInfo, viaInfo, rpcInfo, spxInfo, adspInfo, bvInfo, otherParameters];
+
+ foreach (string? param in allParams)
+ {
+ if (param is not null)
+ {
+ protocolParameters.Add(param);
+ }
+ }
+
+ return string.Join(";", protocolParameters);
+ }
+}