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); + } +}