From 0526e317e09e9bd82ca523cdbb450b10f4a718f4 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:59:24 +0100 Subject: [PATCH 01/11] Update DAC response test data These data are issued as unicast responses, and we should recognise the first response, not the last. --- .../Microsoft/Data/Sql/SsrpPacketTestData.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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 index 9729c33949..1ebfe698fc 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs @@ -179,7 +179,7 @@ public static TheoryData, int> ValidSvrRespDacPacketBuffe validPacket2.AsSpan(0, 2).ToArray(), validPacket2.AsSpan(2).ToArray(), [0x05]), - ValidTcpPort3 + ValidTcpPort2 }, { // Four responses, each with different DAC ports @@ -187,15 +187,15 @@ public static TheoryData, int> ValidSvrRespDacPacketBuffe validPacket2, validPacket3, validPacket4), - ValidTcpPort5 + ValidTcpPort2 }, { // Five responses, with response three invalid - GeneratePacketBuffers(validPacket1, + GeneratePacketBuffers(validPacket4, validPacket2, invalidPacket1, validPacket3, - validPacket4), + validPacket1), ValidTcpPort5 }, { @@ -205,6 +205,13 @@ public static TheoryData, int> ValidSvrRespDacPacketBuffe [0x05], [0x05, ..validPacket3], validPacket4), + ValidTcpPort2 + }, + { + // Two responses, with three extraneous 0x05 bytes before the first + GeneratePacketBuffers([0x05, 0x05], + [0x05, ..validPacket4], + validPacket1), ValidTcpPort5 } }; From fa505b09e93f50f52a9a7777002b1a630e0237b6 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:33:50 +0100 Subject: [PATCH 02/11] Add IndexOf utility function --- .../Data/Common/ReadOnlySequenceUtilities.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ReadOnlySequenceUtilities.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ReadOnlySequenceUtilities.cs index 792ca6ddfc..0de898c279 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ReadOnlySequenceUtilities.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ReadOnlySequenceUtilities.cs @@ -89,4 +89,45 @@ public static bool ReadLittleEndian(this ref ReadOnlySequence sequence, re return true; } } + + /// + /// Find the absolute position of in . + /// + /// The type of each element in . + /// The sequence to search. + /// The value to search for. + /// The absolute position of . + /// + /// This is similar to . However, this returns the + /// number of elements prior to the first appearance of , while PositionOf + /// returns a . + /// + public static long IndexOf(this in ReadOnlySequence sequence, T value) + where T : IEquatable + { + if (sequence.IsSingleSegment) + { + return sequence.First.Span.IndexOf(value); + } + + long cumulativeIndex = 0; + + SequencePosition position = sequence.Start; + while (sequence.TryGet(ref position, out ReadOnlyMemory currChunk, advance: true)) + { + int idx = currChunk.Span.IndexOf(value); + + if (idx != -1) + { + cumulativeIndex += idx; + return cumulativeIndex; + } + else + { + cumulativeIndex += currChunk.Length; + } + } + + return -1; + } } From 9f951f0eb301521853835c2eae93d06c8be6cc52 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:06:36 +0100 Subject: [PATCH 03/11] Add more test cases to DAC response test data These relate to invalid data and empty packet buffers --- .../Microsoft/Data/Sql/SsrpPacketTestData.cs | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) 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 index 1ebfe698fc..fdb71515b7 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs @@ -162,11 +162,13 @@ public static TheoryData, int> ValidSvrRespDacPacketBuffe ValidTcpPort2 }, { - // One response, split into three buffers. + // One response, split into four 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. + // Buffer 2: empty + // Buffer 3: the second byte of RESP_SIZE and the first byte of RESP_DATA (protocol version). + // Buffer 4: remainder. GeneratePacketBuffers(validPacket1.AsSpan(0, 2).ToArray(), + [], validPacket1.AsSpan(2, 2).ToArray(), validPacket1.AsSpan(4).ToArray()), ValidTcpPort2 @@ -213,7 +215,26 @@ public static TheoryData, int> ValidSvrRespDacPacketBuffe [0x05, ..validPacket4], validPacket1), ValidTcpPort5 - } + }, + { + // Two responses, with three garbage bytes before the first + GeneratePacketBuffers([0x01, 0x01], + [0x01, ..validPacket4], + validPacket1), + ValidTcpPort5 + }, + { + // One response, followed by three garbage bytes + GeneratePacketBuffers(validPacket4, + [0x01, 0x01, 0x05]), + ValidTcpPort5 + }, + { + // One empty buffer, followed by one buffer containing one response + GeneratePacketBuffers([], + validPacket1), + ValidTcpPort2 + }, }; } } @@ -242,7 +263,13 @@ public static TheoryData, int> ValidSvrRespDacPacketBuffe // Invalid port GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader, ValidRespDataDacResponseSize, - CreateRespData(ValidRespDataDacProtocolVersion, 0))) + CreateRespData(ValidRespDataDacProtocolVersion, 0))), + + // Invalid port, followed by trailing data + GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader, + ValidRespDataDacResponseSize, + CreateRespData(ValidRespDataDacProtocolVersion, 0)), + [0x00, 0x00, 0x00, 0x00]), ]; /// From 863d1f4aec07484d0428c353dd1b8a19628517b5 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:45:50 +0100 Subject: [PATCH 04/11] Implement DacResponse and DacResponseReader --- .../src/Microsoft/Data/Sql/DacResponse.cs | 101 ++++++++++++++++++ .../Microsoft/Data/Sql/DacResponseReader.cs | 42 ++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponse.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponseReader.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponse.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponse.cs new file mode 100644 index 0000000000..9896ae1d81 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponse.cs @@ -0,0 +1,101 @@ +// 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.Diagnostics; + +#nullable enable + +namespace Microsoft.Data.Sql; + +/// +/// A single parsed SSRP response. +/// +/// +/// +/// This corresponds to an SVR_RESP (DAC) structure within the MC-SQLR specification. +/// An SVR_RESP (DAC) structure is a byte array with the following layout: +/// +/// SVR_RESP: 1 byte, always 0x05. +/// RESP_SIZE: 2 bytes, always 0x06. Written in little-endian byte order. +/// PROTOCOLVERSION: 1 byte, always 0x01. +/// TCP_DAC_PORT: 2 bytes, the TCP port number that is used for the DAC. Written in little-endian byte order. +/// +/// +internal readonly ref struct DacResponse +{ + private const int ResponseHeaderOffset = 0; + private const int ResponseSizeOffset = ResponseHeaderOffset + sizeof(byte); + private const int ProtocolVersionOffset = ResponseSizeOffset + sizeof(ushort); + private const int DacPortOffset = ProtocolVersionOffset + sizeof(byte); + private const int TotalResponseSize = DacPortOffset + sizeof(ushort); + + private const byte ResponseHeaderValue = 0x05; + private const ushort ResponseSizeValue = 0x06; + private const byte ProtocolVersionValue = 0x01; + + public ushort DacPort { get; } + + private DacResponse(ushort tcpDacPort) + { + DacPort = tcpDacPort; + } + + /// + /// Attempts to parse a single SSRP response from the start of the provided source sequence. + /// + /// The source buffer to read from. + /// The populated SSRP response (or default, if one cannot be found.) + /// The number of bytes to advance by. + /// true if the response was processed, false if not. + public static bool TryParse(ReadOnlySequence sourceSequence, out DacResponse response, out long bytesRead) + { + // Make sure we have enough data to read the header. + if (sourceSequence.Length < TotalResponseSize) + { + bytesRead = sourceSequence.Length; + response = default; + return false; + } + + ReadOnlySequence currSequence = sourceSequence; + ReadOnlySpan currSpan = currSequence.First.Span; + long currOffset = 0; + + // Read and validate the response header. + if (currSequence.ReadByte(ref currSpan, ref currOffset, out byte responseHeader) + // RESP_SVR must be 0x05. + && responseHeader == ResponseHeaderValue + && currSequence.ReadLittleEndian(ref currSpan, ref currOffset, out ushort responseSize) + // RESP_SIZE must be 0x0006. + && responseSize == ResponseSizeValue + && currSequence.ReadByte(ref currSpan, ref currOffset, out byte protocolVersion) + // PROTOCOLVERSION must be 0x01. + && protocolVersion == ProtocolVersionValue + && currSequence.ReadLittleEndian(ref currSpan, ref currOffset, out ushort tcpDacPort) + // TCP_DAC_PORT must be non-zero. + && tcpDacPort != 0) + { + Debug.Assert(currOffset == TotalResponseSize); + + bytesRead = currOffset; + response = new DacResponse(tcpDacPort); + return true; + } + else + { + // Find the next possible response start across the sequence. Always advance by one byte (to skip the current + // header byte.) + long idx = sourceSequence.Slice(1).IndexOf(ResponseHeaderValue); + + bytesRead = idx == -1 + ? sourceSequence.Length + : idx + 1; + response = default; + return false; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponseReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponseReader.cs new file mode 100644 index 0000000000..93db678255 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponseReader.cs @@ -0,0 +1,42 @@ +// 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; + +#nullable enable + +namespace Microsoft.Data.Sql; + +/// +/// Utilities used to extract zero or more parsed SSRP DAC responses from a set of packet buffers. +/// +internal static class DacResponseReader +{ + /// + /// Returns the first SSRP response from the provided source sequence, if one exists. + /// + /// The source sequence of buffers from the network. + /// The first SSRP response (if available.) + /// true if an SSRP response can be located in the source sequence, false otherwise. + public static bool TryReadFirst(ReadOnlySequence sourceSequence, out DacResponse firstResponse) + { + ReadOnlySequence remainingSequence = sourceSequence; + + while (!remainingSequence.IsEmpty) + { + bool parsedCurrentRequest = DacResponse.TryParse(remainingSequence, out DacResponse current, out long bytesRead); + + if (parsedCurrentRequest) + { + firstResponse = current; + return true; + } + + remainingSequence = remainingSequence.Slice(bytesRead); + } + + firstResponse = default; + return false; + } +} From 7a7cec17abfbd61f238c36f1eff3119deefd9b75 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:47:07 +0100 Subject: [PATCH 05/11] Implement DacResponseReader tests --- .../Data/Sql/DacResponseProcessorTest.cs | 33 -------------- .../Data/Sql/DacResponseReaderTest.cs | 43 +++++++++++++++++++ .../Microsoft/Data/Sql/SsrpPacketTestData.cs | 4 +- 3 files changed, 45 insertions(+), 35 deletions(-) delete mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseProcessorTest.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseReaderTest.cs 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 deleted file mode 100644 index fe56f8c5d5..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseProcessorTest.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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/DacResponseReaderTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseReaderTest.cs new file mode 100644 index 0000000000..e1bf832113 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseReaderTest.cs @@ -0,0 +1,43 @@ +// 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; + +#nullable enable + +namespace Microsoft.Data.Sql.UnitTests; + +public class DacResponseProcessorTest +{ + [Theory] + [MemberData(nameof(SsrpPacketTestData.EmptyPacketBuffer), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)] + public void Read_EmptyBuffer_ReturnsFalse(ReadOnlySequence packetBuffers) + { + bool containsDacResponse = DacResponseReader.TryReadFirst(packetBuffers, out DacResponse response); + + Assert.False(containsDacResponse); + Assert.Equal(0, response.DacPort); + } + + [Theory] + [MemberData(nameof(SsrpPacketTestData.InvalidSvrRespDacPackets), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)] + public void Read_InvalidDacResponse_ReturnsFalse(ReadOnlySequence packetBuffers) + { + bool containsDacResponse = DacResponseReader.TryReadFirst(packetBuffers, out DacResponse response); + + Assert.False(containsDacResponse); + Assert.Equal(0, response.DacPort); + } + + [Theory] + [MemberData(nameof(SsrpPacketTestData.ValidSvrRespDacPacketBuffer), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)] + public void Read_ValidDacResponse_ReturnsData(ReadOnlySequence packetBuffers, int expectedDacPort) + { + bool containsDacResponse = DacResponseReader.TryReadFirst(packetBuffers, out DacResponse response); + + Assert.True(containsDacResponse); + Assert.Equal(expectedDacPort, response.DacPort); + } +} 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 index fdb71515b7..dad0d67044 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs @@ -34,7 +34,7 @@ internal static class SsrpPacketTestData /// /// One empty packet buffer, which should be successfully processed and contain zero responses. /// - /// + /// /// public static TheoryData> EmptyPacketBuffer => new(GeneratePacketBuffers([])); @@ -242,7 +242,7 @@ public static TheoryData, int> ValidSvrRespDacPacketBuffe /// /// Packet buffers containing nothing but invalid SVR_RESP (DAC) responses. /// - /// + /// public static TheoryData> InvalidSvrRespDacPackets => [ // Invalid header byte From d648939973a043760f9de8bb64e517223e5411d0 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:52:59 +0100 Subject: [PATCH 06/11] Documentation updates and debug assertion Add assertion on bytesRead in DacResponseReader --- .../src/Microsoft/Data/Sql/DacResponse.cs | 8 ++++++++ .../src/Microsoft/Data/Sql/DacResponseReader.cs | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponse.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponse.cs index 9896ae1d81..c58ab8963b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponse.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponse.cs @@ -46,11 +46,19 @@ private DacResponse(ushort tcpDacPort) /// /// Attempts to parse a single SSRP response from the start of the provided source sequence. + /// If an SSRP response cannot be found, supplies the maximum number of bytes to advance the + /// sequence by before attempting to parse another response. /// /// The source buffer to read from. /// The populated SSRP response (or default, if one cannot be found.) /// The number of bytes to advance by. /// true if the response was processed, false if not. + /// + /// If the sequence does not start with an SSRP response, will + /// contain the position of the next possible SVR_RESP header byte (0x05), or + /// the length of if this header byte is not present in the + /// sequence. + /// public static bool TryParse(ReadOnlySequence sourceSequence, out DacResponse response, out long bytesRead) { // Make sure we have enough data to read the header. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponseReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponseReader.cs index 93db678255..b9541614fb 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponseReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/DacResponseReader.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Buffers; +using System.Diagnostics; #nullable enable @@ -33,6 +34,11 @@ public static bool TryReadFirst(ReadOnlySequence sourceSequence, out DacRe return true; } + // If the current request cannot be parsed, advance by bytesRead. + // bytesRead will always be greater than zero - it'll be the next possibly-viable + // position of a DAC response, or the end of the sequence if no viable DAC responses + // can be found. + Debug.Assert(bytesRead > 0 && bytesRead <= remainingSequence.Length); remainingSequence = remainingSequence.Slice(bytesRead); } From 3695a007a233136692a7e0cec64e9369e77931cc Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 28 Jul 2026 06:36:40 +0100 Subject: [PATCH 07/11] Implement SqlDataSourceResponse and SqlDataSourceResponseReader --- .../Data/Sql/SqlDataSourceResponse.cs | 651 ++++++++++++++++++ .../Data/Sql/SqlDataSourceResponseReader.cs | 93 +++ .../System/Text/EncodingExtensions.netfx.cs | 11 +- 3 files changed, 752 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponseReader.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs new file mode 100644 index 0000000000..345009d1c7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs @@ -0,0 +1,651 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Text; + +#nullable enable + +namespace Microsoft.Data.Sql; + +/// +/// A single parsed SSRP response. +/// +/// +/// +/// This corresponds to an SVR_RESP structure within the MC-SQLR specification. +/// An SVR_RESP (DAC) structure is a byte array with the following layout: +/// +/// SVR_RESP: 1 byte, always 0x05. +/// RESP_SIZE: 2 bytes, length of the RESP_DATA data. Written in little-endian byte order. +/// RESP_DATA: variable length, up to 1024 bytes if responding to CLNT_UCAST_INST or 65535 bytes if responding to CLNT_UCAST_EX. +/// +/// +internal readonly ref struct SqlDataSourceResponse +{ + // Constants for offsets in the fixed-length fields in the SSRP header. + private const int ResponseHeaderOffset = 0; + private const int ResponseSizeOffset = ResponseHeaderOffset + sizeof(byte); + private const int RespDataOffset = ResponseSizeOffset + sizeof(ushort); + private const int ConstantHeaderSize = RespDataOffset; + + private const byte ResponseHeaderValue = 0x05; + + private static readonly Encoding s_mbcsEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + + /// + /// Transport struct used to process a specific component of the RESP_DATA response. + /// Users of this struct must use the TryGetX methods, or check that + /// is true before interpreting the property. + /// + /// + /// + /// A component is a combination of a key and a value. A + /// value may consist of multiple tokens. The key and the value are separated + /// by a ; character. Each token in the value is also separated by a ; character. + /// + /// + /// relates strictly to structural validity. It indicates solely + /// that this component is the correctly-parsed product of a RESP_DATA string. It does not + /// indicate that the value is valid for the key (or safe for user consumption.)Please + /// + /// + private readonly ref struct RespDataComponent + { + // Maximum lengths of the values of the RESP_DATA components of the response. + // Measured in bytes. + public const long MaxServerNameLength = 255; + public const long MaxInstanceNameLength = 255; + public const long MaxIsClusteredLength = 255; + public const long MaxVersionLength = 16; + + public ReadOnlySpan Key { get; } + + public ReadOnlySpan Value { get; } + + public bool Valid { get; } + + private RespDataComponent(ReadOnlySpan key, ReadOnlySpan value) + { + Key = key; + Value = value; + Valid = true; + } + + /// + /// Parses a RESP_DATA component containing a key and one value, starting from position + /// and incrementing this position following the parsing of the + /// component. + /// + /// The entire RESP_DATA string. + /// Position to start parsing within this string (in characters.) + /// Maximum length of the component's value, or -1 if no limit exists. + /// The parsed component. + /// true if the RESP_DATA component was parsed, false if not. + /// + /// Note that a parsed component is a key/value pair which is structurally correct. It does + /// not imply that the value is actually valid. To enforce this, access the value via the + /// TryGetX methods or check that is true before accessing the + /// property. + /// + public static bool TryParse(string respData, scoped ref int startPos, long maxValueLength, out RespDataComponent token) + => TryParse(respData, ref startPos, maxValueLength, expectedTokensInValue: 1, out token); + + /// + /// Parses a RESP_DATA component which contains a key and exactly + /// values, starting from position and incrementing this position + /// following the parsing of the component. + /// + /// + /// The entire RESP_DATA string. + /// Position to start parsing within this string (in characters.) + /// Maximum length (in bytes) of the component's value, or -1 if no limit exists. + /// The number of ;-delimited tokens in the value. + /// The parsed component. + /// true if the RESP_DATA component was parsed, false if not. + /// + /// MC-SQLR specifies that certain RESP_DATA component values have a maximum length. As a + /// result, will specify the start position of the component in + /// characters, but will specify the maximum length of the + /// component in bytes. + /// + public static bool TryParse(string respData, scoped ref int startPos, long maxValueLength, int expectedTokensInValue, out RespDataComponent token) + { + const char Terminator = ';'; + + ReadOnlySpan respDataSpan = respData.AsSpan(startPos); + // The buffer must start with the key, and the key must be followed by a terminator. + // Following the terminator comes a value, which must also be followed by a terminator. + int terminatorPos = respDataSpan.IndexOf(Terminator); + + if (terminatorPos == -1) + { + token = default; + return false; + } + + ReadOnlySpan keyCandidate = respDataSpan.Slice(0, terminatorPos); + ReadOnlySpan valueCandidate = respDataSpan.Slice(terminatorPos + 1); + + // There could potentially be a number of terminators within the RESP_DATA token's value. The Banyan VINES parameters contain five. + // BV_PARAMETERS = "bv;;;;;;" + // Accounting for this here means that we don't run the risk of being interpreted as a key in its own right. + int valueTerminatorPosition = 0; + ReadOnlySpan trailingValue = valueCandidate; + int tokenCount = 0; + + for (int i = 0; i < expectedTokensInValue; i++) + { + int nextTerminatorPos = trailingValue.IndexOf(Terminator); + + if (nextTerminatorPos == -1) + { + break; + } + + tokenCount++; + valueTerminatorPosition += nextTerminatorPos + 1; + trailingValue = trailingValue.Slice(nextTerminatorPos + 1); + } + + if (tokenCount != expectedTokensInValue) + { + token = default; + return false; + } + + terminatorPos = valueTerminatorPosition - 1; + valueCandidate = valueCandidate.Slice(0, terminatorPos); + + // If a maximum value length is specified, this is in bytes. Calculate the number of + // bytes in the string, and compare. + if (maxValueLength != -1) + { + int valueByteCount = Encoding.UTF8.GetByteCount(valueCandidate); + + if (valueByteCount > maxValueLength) + { + token = default; + return false; + } + } + + startPos += keyCandidate.Length + 1 + valueCandidate.Length + 1; + + token = new RespDataComponent(keyCandidate, valueCandidate); + return true; + } + + public bool TryGetBoolean(out bool value) + { + value = false; + + if (Valid) + { + if (Value.Equals("Yes".AsSpan(), StringComparison.Ordinal)) + { + value = true; + return true; + } + else if (Value.Equals("No".AsSpan(), StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + public bool TryGetVersion([NotNullWhen(true)] out Version? value) + { + value = null; +#if NET + return Valid && Version.TryParse(Value, out value); +#else + return Valid && Version.TryParse(Value.ToString(), out value); +#endif + } + + public bool TryGetUInt16(out ushort value) + { + value = 0; + +#if NET + return Valid && ushort.TryParse(Value, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out value); +#else + return Valid && ushort.TryParse(Value.ToString(), System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out value); +#endif + } + + public bool TryGetServerName(out ReadOnlySpan value) + { + bool valid = Valid && Value.Length > 0; + + if (valid) + { + foreach (char ch in Value) + { + if (ch != '.' + && ch != '-' + && (!(ch >= '0' && ch <= '9')) + && (!(ch >= 'A' && ch <= 'Z')) + && (!(ch >= 'a' && ch <= 'z'))) + { + valid = false; + break; + } + } + } + value = valid ? Value : []; + return valid; + } + + public bool TryGetInstanceName(out ReadOnlySpan value) + { + bool valid = Valid && Value.Length > 0; + + if (valid) + { + foreach (char ch in Value) + { + if (char.IsControl(ch)) + { + valid = false; + break; + } + } + } + value = valid ? Value : []; + return valid; + } + } + + /// + /// Transport struct used to collate all library-relevant protocol metadata. + /// + /// + /// + /// Users must verify that is true before reading + /// or that is true before reading . + /// If is false, no protocol metadata is present and the protocol + /// details are invalid - this SSRP response is unusable, because we can't connect in any + /// relevant way. + /// + /// + /// A successfully parsed instance does not guarantee secure data. Callers must ensure that + /// points to the same server identified by the SSRP response's server + /// name before they connect to it. + /// + /// + private readonly ref struct ExposedProtocols + { + public bool TcpEnabled { get; } + + public ushort TcpPort { get; } + + public bool NamedPipeEnabled { get; } + + public ReadOnlySpan NamedPipe { get; } + + public bool Valid => TcpEnabled || NamedPipeEnabled; + + public ExposedProtocols(RespDataComponent tcpToken, RespDataComponent npToken) + { + ushort tcpPort = 0; + + TcpEnabled = tcpToken.Valid && tcpToken.TryGetUInt16(out tcpPort) && tcpPort != 0; + if (TcpEnabled) + { + TcpPort = tcpPort; + } + + NamedPipeEnabled = npToken.Valid && !npToken.Value.IsEmpty; + if (NamedPipeEnabled) + { + NamedPipe = npToken.Value; + } + } + } + + public ReadOnlySpan ServerName { get; } + + public ReadOnlySpan InstanceName { get; } + + public bool IsClustered { get; } + + public Version Version { get; } + + public bool TcpEnabled { get; } + + public ushort TcpPort { get; } + + public bool NamedPipeEnabled { get; } + + public ReadOnlySpan NamedPipe { get; } + + private SqlDataSourceResponse(ReadOnlySpan serverName, ReadOnlySpan instanceName, + bool isClustered, Version version, + ExposedProtocols protocols) + { + ServerName = serverName; + InstanceName = instanceName; + IsClustered = isClustered; + Version = version; + + TcpEnabled = protocols.TcpEnabled; + TcpPort = protocols.TcpPort; + NamedPipeEnabled = protocols.NamedPipeEnabled; + NamedPipe = protocols.NamedPipe; + } + + /// + /// Attempts to parse a single SSRP response from the start of the provided source sequence. + /// If an SSRP response cannot be found, supplies the maximum number of bytes to advance the + /// sequence by before attempting to parse another response. + /// + /// The source buffer to read from. + /// The maximum allowed size for the RESP_DATA section of the response. + /// The populated SSRP response (or default, if one cannot be found.) + /// The number of bytes to advance by. + /// true if the response was processed, false if not. + /// + /// If the sequence does not start with an SSRP response, will + /// contain the position of the next possible SVR_RESP header byte (0x05), or + /// the length of if this header byte is not present in the + /// sequence. + /// + public static bool TryParse(ReadOnlySequence sourceSequence, ushort maxDynamicDataSize, out SqlDataSourceResponse response, out long bytesRead) + { + // Make sure we have enough data to read the constant size of the header. + if (sourceSequence.Length < ConstantHeaderSize) + { + bytesRead = sourceSequence.Length; + response = default; + return false; + } + + ReadOnlySequence currSequence = sourceSequence; + ReadOnlySpan currSpan = sourceSequence.First.Span; + long currOffset = 0; + + // Read and validate the constant part of the response header. + if (currSequence.ReadByte(ref currSpan, ref currOffset, out byte responseHeader) + // RESP_SVR must be 0x05. + && responseHeader == ResponseHeaderValue + && currSequence.ReadLittleEndian(ref currSpan, ref currOffset, out ushort responseSize) + // RESP_SIZE must be greater than 0 and fit within the source sequence. + // It must also not exceed the maximum allowed dynamic data size. + && responseSize > 0 + && (currOffset + responseSize) <= sourceSequence.Length + && responseSize <= maxDynamicDataSize + // RESP_DATA must successfully parse. + && TryParseRespData(ref currSequence, ref currOffset, responseSize, out response)) + { + bytesRead = currOffset; + return true; + } + else + { + // Find the next possible response start across the sequence. Always advance by one byte (to skip the current + // header byte.) + long idx = sourceSequence.Slice(1).IndexOf(ResponseHeaderValue); + + bytesRead = idx == -1 + ? sourceSequence.Length + : idx + 1; + response = default; + return false; + } + } + + private static bool TryParseRespData(scoped ref ReadOnlySequence sequence, scoped ref long currOffset, ushort responseSize, out SqlDataSourceResponse response) + { + const string ServerNameKey = "ServerName"; + const string InstanceNameKey = "InstanceName"; + const string IsClusteredKey = "IsClustered"; + const string VersionKey = "Version"; + + // Make sure we have enough data to read the constant mandatory header components. + ushort minLength = (ushort)(ServerNameKey.Length + 1 + InstanceNameKey.Length + 1 + + IsClusteredKey.Length + 1 + VersionKey.Length + 1); + + if (responseSize < minLength) + { + response = default; + return false; + } + + int currRespDataOffset = 0; + string decodedRespData; + + try + { +#if NET + decodedRespData = s_mbcsEncoding.GetString(sequence.Slice(0, responseSize)); +#else + decodedRespData = s_mbcsEncoding.GetString(sequence.Slice(0, responseSize).ToArray()); +#endif + } + catch (DecoderFallbackException) + { + response = default; + return false; + } + + // Parse and validate the header components + if (!RespDataComponent.TryParse(decodedRespData, ref currRespDataOffset, RespDataComponent.MaxServerNameLength, out RespDataComponent serverNameToken) + // The first token must be ServerName. + || !serverNameToken.Key.Equals(ServerNameKey.AsSpan(), StringComparison.Ordinal) + // The ServerName token must be a valid FQDN. + // Note: This validation is stricter than called for in MC-SQLR. It is designed to + // ensure that server names only contain valid characters (".", "-", a-z, A-Z, 0-9). + || !serverNameToken.TryGetServerName(out ReadOnlySpan serverName) + || !RespDataComponent.TryParse(decodedRespData, ref currRespDataOffset, RespDataComponent.MaxInstanceNameLength, out RespDataComponent instanceNameToken) + // The second token must be InstanceName. + || !instanceNameToken.Key.Equals(InstanceNameKey.AsSpan(), StringComparison.Ordinal) + // The InstanceName token must be valid. + // Note: This validation is stricter than called for in MC-SQLR, but more lenient + // than a server name. Its sole goal is to block control characters + || !instanceNameToken.TryGetInstanceName(out ReadOnlySpan instanceName) + || !RespDataComponent.TryParse(decodedRespData, ref currRespDataOffset, RespDataComponent.MaxIsClusteredLength, out RespDataComponent isClusteredToken) + // The third token must be IsClustered. + || !isClusteredToken.Key.Equals(IsClusteredKey.AsSpan(), StringComparison.Ordinal) + // The IsClustered token must be Yes or No. + || !isClusteredToken.TryGetBoolean(out bool isClustered) + || !RespDataComponent.TryParse(decodedRespData, ref currRespDataOffset, RespDataComponent.MaxVersionLength, out RespDataComponent versionToken) + // The fourth token must be Version. + || !versionToken.Key.Equals(VersionKey.AsSpan(), StringComparison.Ordinal) + // The Version token must be a valid version. + || !versionToken.TryGetVersion(out Version? version)) + { + response = default; + return false; + } + + // Now, move to the dynamic data. Iterate over each of the following keys, performing basic checks to ensure that each key only appears once and + // looking for our key protocols: TCP and Named Pipes. + if (!TryParseRespProtocolData(decodedRespData, ref currRespDataOffset, + out int tcpTokenOffset, out int npTokenOffset)) + { + response = default; + return false; + } + + // Try to parse the TCP and the named pipes tokens, if they exist. + RespDataComponent tcpToken = default; + RespDataComponent npToken = default; + + if (tcpTokenOffset != -1 + && !RespDataComponent.TryParse(decodedRespData, ref tcpTokenOffset, maxValueLength: -1, out tcpToken)) + { + response = default; + return false; + } + + if (npTokenOffset != -1 + && !RespDataComponent.TryParse(decodedRespData, ref npTokenOffset, maxValueLength: -1, out npToken)) + { + response = default; + return false; + } + + // With both relevant tokens tested for existence, ensure that the combination of them is valid. + // We must ensure that at least one of them is available, but that both of them are valid, if present. + ExposedProtocols protocols = new(tcpToken, npToken); + if (!protocols.Valid) + { + response = default; + return false; + } + + // All RESP_DATA has been successfully parsed. Ensure that nothing remains in the string except for the final trailing ";". + // We've already validated that the final value ends with a ";", so we're actually verifying that the string as a whole ends with ";;". + if (currRespDataOffset != decodedRespData.Length - 1 + || decodedRespData[currRespDataOffset] != ';') + { + response = default; + return false; + } + + currOffset += responseSize; + sequence = sequence.Slice(responseSize); + response = new SqlDataSourceResponse(serverName, instanceName, isClustered, version, protocols); + + return true; + } + + private static bool TryParseRespProtocolData(string respData, ref int currPos, out int tcpTokenOffset, out int npTokenOffset) + { + const string NamedPipesInfoKey = "np"; + const string TcpInfoKey = "tcp"; + const string ViaInfoKey = "via"; + const string RpcInfoKey = "rpc"; + const string SpxInfoKey = "spx"; + const string AdspInfoKey = "adsp"; + const string BanyanVinesInfoKey = "bv"; + + int tempCurrPos = currPos; + + bool npKeyFound = false; + bool tcpKeyFound = false; + bool viaKeyFound = false; + bool rpcKeyFound = false; + bool spxKeyFound = false; + bool adspKeyFound = false; + bool bvKeyFound = false; + + tcpTokenOffset = -1; + npTokenOffset = -1; + + while (RespDataComponent.TryParse(respData, ref tempCurrPos, maxValueLength: -1, out RespDataComponent currentToken)) + { + // If we encounter a BV_INFO key, then we need to set the position back to currPos and + // re-parse allowing for extra separators. This is because a BV_INFO key has a value in + // the format ";;;;" and failing + // to reparse would mean that we would see the group name parsed as the next key. + // Technically, the extra separators would permit text which looks like another protocol + // key to be smuggled into BV_INFO. This parser ignores such text. The example below will + // be treated as a BV_INFO key, not as a duplicate TCP_INFO key: + // "bv;ITEM1;tcp;ITEM1;tcp;80;" + // is ITEM1, is tcp, is 80. + if (currentToken.Key.Equals(BanyanVinesInfoKey.AsSpan(), StringComparison.Ordinal)) + { + tempCurrPos = currPos; + + if (!RespDataComponent.TryParse(respData, ref tempCurrPos, maxValueLength: -1, expectedTokensInValue: 5, out currentToken)) + { + break; + } + } + + // Each protocol may only appear once, as per MC-SQLR, section 2.2.5, Note 1. + bool protocolSpecifiedTwice = !TrySetFlag(currentToken.Key, ref npKeyFound, + ref tcpKeyFound, ref viaKeyFound, ref rpcKeyFound, + ref spxKeyFound, ref adspKeyFound, ref bvKeyFound); + + if (protocolSpecifiedTwice) + { + return false; + } + + // Processing TCP_INFO. The TCP_PORT parameter is mandatory. + // It must also be a number in the 1-65535 range. + if (currentToken.Key.Equals(TcpInfoKey.AsSpan(), StringComparison.Ordinal)) + { + tcpTokenOffset = currPos; + } + // Processing NP_INFO. The PIPENAME parameter is mandatory. + else if (currentToken.Key.Equals(NamedPipesInfoKey.AsSpan(), StringComparison.Ordinal)) + { + npTokenOffset = currPos; + } + // Other protocols also appear, and have their own validation logic. This typically + // involves maximum lengths (in either bytes or characters) or expected formats. This + // parser explicitly does not verify these - downstream clients can only connect via + // TCP or named pipes. + + currPos = tempCurrPos; + } + + return true; + + static bool TrySetFlag(ReadOnlySpan key, + ref bool npKeyFound, ref bool tcpKeyFound, + ref bool viaKeyFound, ref bool rpcKeyFound, + ref bool spxKeyFound, ref bool adspKeyFound, + ref bool bvKeyFound) + { + bool dummyFlag = true; + ref bool relevantFlag = ref dummyFlag; + + // relevantFlag is a managed reference to the appropriate parameter. This parameter is + // selected based upon the key - a protocol name. A failure to match a known key will + // allow the default value (a managed reference to a boolean true) value to stand, forcing + // this method to return false. + if (key.Equals(NamedPipesInfoKey.AsSpan(), StringComparison.Ordinal)) + { + relevantFlag = ref npKeyFound; + } + else if (key.Equals(TcpInfoKey.AsSpan(), StringComparison.Ordinal)) + { + relevantFlag = ref tcpKeyFound; + } + else if (key.Equals(ViaInfoKey.AsSpan(), StringComparison.Ordinal)) + { + relevantFlag = ref viaKeyFound; + } + else if (key.Equals(RpcInfoKey.AsSpan(), StringComparison.Ordinal)) + { + relevantFlag = ref rpcKeyFound; + } + else if (key.Equals(SpxInfoKey.AsSpan(), StringComparison.Ordinal)) + { + relevantFlag = ref spxKeyFound; + } + else if (key.Equals(AdspInfoKey.AsSpan(), StringComparison.Ordinal)) + { + relevantFlag = ref adspKeyFound; + } + else if (key.Equals(BanyanVinesInfoKey.AsSpan(), StringComparison.Ordinal)) + { + relevantFlag = ref bvKeyFound; + } + + // If key is not as we expect, or if the key's flag is already set, return false. + // Otherwise, set the flag and return true. + if (relevantFlag) + { + return false; + } + else + { + relevantFlag = true; + return true; + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponseReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponseReader.cs new file mode 100644 index 0000000000..57d81920b5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponseReader.cs @@ -0,0 +1,93 @@ +// 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 System.Diagnostics; + +#nullable enable + +namespace Microsoft.Data.Sql; + +/// +/// Utilities used to extract zero or more unparsed SSRP responses from a single source buffer. +/// +internal static class SqlDataSourceResponseReader +{ + /// + /// Returns the first SSRP response from the provided source sequence, if one exists. + /// + /// The source sequence of buffers from the network. + /// The first SSRP response (if available.) + /// true if an SSRP response can be located in the source sequence, false otherwise. + /// + /// This method parses SVR_RESP messages sent as the result of issuing a CLNT_UCAST_INST message. + /// + public static bool TryReadFirst(ReadOnlySequence sourceSequence, out SqlDataSourceResponse firstResponse) + { + const ushort MaxDynamicDataSize = 1024; + + ReadOnlySequence remainingSequence = sourceSequence; + + while (!remainingSequence.IsEmpty) + { + bool parsedCurrentRequest = SqlDataSourceResponse.TryParse(remainingSequence, MaxDynamicDataSize, out SqlDataSourceResponse current, out long bytesRead); + + if (parsedCurrentRequest) + { + firstResponse = current; + return true; + } + + // If the current request cannot be parsed, advance by bytesRead. + // bytesRead will always be greater than zero - it'll be the next possibly-viable + // position of an SSRP response, or the end of the sequence if no viable SSRP responses + // can be found. + Debug.Assert(bytesRead > 0 && bytesRead <= remainingSequence.Length); + remainingSequence = remainingSequence.Slice(bytesRead); + } + + firstResponse = default; + return false; + } + + /// + /// Returns the final SSRP response from the provided source sequence, if one exists. + /// + /// The source sequence of buffers from the network. + /// The final SSRP response (if available.) + /// true if at least one SSRP response is present in the source sequence, false otherwise. + /// + /// This method parses SVR_RESP messages sent as the result of issuing a CLNT_BCAST_EX or a CLNT_UCAST_EX + /// message. + /// + public static bool TryReadLast(ReadOnlySequence sourceSequence, out SqlDataSourceResponse lastResponse) + { + const ushort MaxDynamicDataSize = 65535; + + ReadOnlySequence remainingSequence = sourceSequence; + SqlDataSourceResponse lastGoodResponse = default; + bool responseExists = false; + bool parsedCurrentRequest; + + while (!remainingSequence.IsEmpty) + { + parsedCurrentRequest = SqlDataSourceResponse.TryParse(remainingSequence, MaxDynamicDataSize, out SqlDataSourceResponse current, out long bytesRead); + + // If the current request cannot be parsed, advance by bytesRead. + // bytesRead will always be greater than zero - it'll be the next possibly-viable + // position of an SSRP response, or the end of the sequence if no viable SSRP responses + // can be found. + Debug.Assert(bytesRead > 0 && bytesRead <= remainingSequence.Length); + remainingSequence = remainingSequence.Slice(bytesRead); + if (parsedCurrentRequest) + { + responseExists = true; + lastGoodResponse = current; + } + } + + lastResponse = responseExists ? lastGoodResponse : default; + return responseExists; + } +} diff --git a/src/Microsoft.Data.SqlClient/src/System/Text/EncodingExtensions.netfx.cs b/src/Microsoft.Data.SqlClient/src/System/Text/EncodingExtensions.netfx.cs index baf2a275e5..28af3a45f6 100644 --- a/src/Microsoft.Data.SqlClient/src/System/Text/EncodingExtensions.netfx.cs +++ b/src/Microsoft.Data.SqlClient/src/System/Text/EncodingExtensions.netfx.cs @@ -21,16 +21,21 @@ public static int GetByteCount(this Encoding encoding, string? s, int offset, in ReadOnlySpan slicedString = s.AsSpan(offset, count); - if (slicedString.Length == 0) + return GetByteCount(encoding, slicedString); + } + + public static int GetByteCount(this Encoding encoding, ReadOnlySpan chars) + { + if (chars.Length == 0) { return 0; } unsafe { - fixed (char* str = slicedString) + fixed (char* str = chars) { - return encoding.GetByteCount(str, slicedString.Length); + return encoding.GetByteCount(str, chars.Length); } } } From 3806b1c98fa5bd638739cc1d74bab5e66f4b3e2c Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 28 Jul 2026 06:36:56 +0100 Subject: [PATCH 08/11] Add new test cases --- .../Microsoft/Data/Sql/SsrpPacketTestData.cs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) 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 index dad0d67044..493a6996d5 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs @@ -43,8 +43,8 @@ internal static class SsrpPacketTestData /// Various combinations of packet buffers containing normal SVR_RESP responses, all of which /// should be successfully processed. /// - /// - public static TheoryData, string, int, string?> ValidSvrRespPacketBuffer + /// + public static TheoryData, string, int, string?, string, int, string?> ValidSvrRespPacketBuffer { get { @@ -57,6 +57,8 @@ internal static class SsrpPacketTestData spxInfo: $"spx;{ValidInstanceName}", adspInfo: "adsp;SQL2000", bvInfo: "bv;item;group;item;group;org"); + string smuggledProtocolParameters = CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort1}", + bvInfo: $"bv;item;tcp;item;tcp;{ValidTcpPort2}"); byte[] complexValidPacket = FormatSvrRespMessage(ValidSvrRespHeader, respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion, complexProtocolParameters)); @@ -68,6 +70,8 @@ internal static class SsrpPacketTestData 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[] validPacket5 = FormatSvrRespMessage(ValidSvrRespHeader, + respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion, smuggledProtocolParameters)); byte[] invalidPacket1 = FormatSvrRespMessage(ValidSvrRespHeader, respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, "v14", CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort1}"))); @@ -78,6 +82,9 @@ internal static class SsrpPacketTestData GeneratePacketBuffers(complexValidPacket), ValidServerVersion, ValidTcpPort1, + PipeName, + ValidServerVersion, + ValidTcpPort1, PipeName }, { @@ -90,6 +97,9 @@ internal static class SsrpPacketTestData complexValidPacket.AsSpan(107).ToArray()), ValidServerVersion, ValidTcpPort1, + PipeName, + ValidServerVersion, + ValidTcpPort1, PipeName }, { @@ -101,6 +111,9 @@ internal static class SsrpPacketTestData validPacket4), ValidServerVersion, ValidTcpPort4, + null, + ValidServerVersion, + ValidTcpPort1, null }, { @@ -113,6 +126,19 @@ internal static class SsrpPacketTestData validPacket4), ValidServerVersion, ValidTcpPort4, + null, + ValidServerVersion, + ValidTcpPort1, + PipeName + }, + { + // One buffer, one response (but with a separate TCP_INFO smuggled inside BV_INFO.) + GeneratePacketBuffers(validPacket5), + ValidServerVersion, + ValidTcpPort1, + null, + ValidServerVersion, + ValidTcpPort1, null } }; From 68edbb3b9164376ca4d7e785b34677832093cb3d Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 28 Jul 2026 06:38:17 +0100 Subject: [PATCH 09/11] Implement and rename SqlDataSourceResponseReaderTest --- .../Sql/SqlDataSourceResponseProcessorTest.cs | 56 --------- .../Sql/SqlDataSourceResponseReaderTest.cs | 114 ++++++++++++++++++ .../Microsoft/Data/Sql/SsrpPacketTestData.cs | 14 +-- 3 files changed, 121 insertions(+), 63 deletions(-) delete mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SqlDataSourceResponseProcessorTest.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SqlDataSourceResponseReaderTest.cs 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 deleted file mode 100644 index 695a8a5cbb..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SqlDataSourceResponseProcessorTest.cs +++ /dev/null @@ -1,56 +0,0 @@ -// 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/SqlDataSourceResponseReaderTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SqlDataSourceResponseReaderTest.cs new file mode 100644 index 0000000000..17f20bc0f9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SqlDataSourceResponseReaderTest.cs @@ -0,0 +1,114 @@ +// 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 SqlDataSourceResponseReaderTest +{ + [Theory] + [MemberData(nameof(SsrpPacketTestData.EmptyPacketBuffer), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)] + public void Read_EmptyBuffer_ReturnsFalse(ReadOnlySequence packetBuffers) + { + bool containsMulticastSsrpResponse = SqlDataSourceResponseReader.TryReadLast(packetBuffers, out _); + bool containsUnicastSsrpResponse = SqlDataSourceResponseReader.TryReadFirst(packetBuffers, out _); + + Assert.False(containsMulticastSsrpResponse); + Assert.False(containsUnicastSsrpResponse); + } + + [Theory] + [MemberData(nameof(SsrpPacketTestData.InvalidSvrRespPackets), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)] + public void Read_InvalidSqlDataSourceResponse_ReturnsFalse(ReadOnlySequence packetBuffers) + { + bool containsMulticastSsrpResponse = SqlDataSourceResponseReader.TryReadLast(packetBuffers, out _); + bool containsUnicastSsrpResponse = SqlDataSourceResponseReader.TryReadFirst(packetBuffers, out _); + + Assert.False(containsMulticastSsrpResponse); + Assert.False(containsUnicastSsrpResponse); + } + + [Theory] + [MemberData(nameof(SsrpPacketTestData.InvalidRespDataPackets), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)] + public void Read_InvalidSqlDataSourceResponse_RespData_ReturnsFalse(ReadOnlySequence packetBuffers) + { + bool containsMulticastSsrpResponse = SqlDataSourceResponseReader.TryReadLast(packetBuffers, out _); + bool containsUnicastSsrpResponse = SqlDataSourceResponseReader.TryReadFirst(packetBuffers, out _); + + Assert.False(containsMulticastSsrpResponse); + Assert.False(containsUnicastSsrpResponse); + } + + [Theory] + [MemberData(nameof(SsrpPacketTestData.InvalidTcpInfoPackets), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)] + public void Read_InvalidSqlDataSourceResponse_TcpInfo_ReturnsFalse(ReadOnlySequence packetBuffers) + { + bool containsMulticastSsrpResponse = SqlDataSourceResponseReader.TryReadLast(packetBuffers, out _); + bool containsUnicastSsrpResponse = SqlDataSourceResponseReader.TryReadFirst(packetBuffers, out _); + + Assert.False(containsMulticastSsrpResponse); + Assert.False(containsUnicastSsrpResponse); + } + + [Theory] + [MemberData(nameof(SsrpPacketTestData.InvalidClntUcastInstSvrRespPackets), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)] + public void Read_InvalidSqlDataSourceResponseToClntUcastInst_ReturnsFalse(ReadOnlySequence packetBuffers) + { + const int MaxRESP_DATASizeForResponseToCLNT_UCAST_INST = 1024; + + bool containsUnicastSsrpResponse = SqlDataSourceResponseReader.TryReadFirst(packetBuffers, out _); + bool containsBroadcastSsrpResponse = SqlDataSourceResponseReader.TryReadLast(packetBuffers, out SqlDataSourceResponse response); + + Assert.False(containsUnicastSsrpResponse); + + Assert.True(containsBroadcastSsrpResponse); + Assert.Equal("srv1", response.ServerName.ToString()); + Assert.Equal("MSSQLSERVER", response.InstanceName.ToString()); + + Assert.True(response.TcpEnabled); + Assert.Equal(1433, response.TcpPort); + Assert.True(response.NamedPipeEnabled); + Assert.True(response.NamedPipe.Length > MaxRESP_DATASizeForResponseToCLNT_UCAST_INST); + } + + [Theory] + [MemberData(nameof(SsrpPacketTestData.ValidSvrRespPacketBuffer), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)] + public void Read_ValidSqlDataSourceResponse_ReturnsData(ReadOnlySequence packetBuffers, + string expectedBroadcastVersion, int expectedBroadcastTcpPort, string? expectedBroadcastPipeName, + string expectedUnicastVersion, int expectedUnicastTcpPort, string? expectedUnicastPipeName) + { + bool containsBroadcastSsrpResponse = SqlDataSourceResponseReader.TryReadLast(packetBuffers, out SqlDataSourceResponse broadcastResponse); + bool containsUnicastSsrpResponse = SqlDataSourceResponseReader.TryReadFirst(packetBuffers, out SqlDataSourceResponse unicastResponse); + + AssertResponse(broadcastResponse, containsBroadcastSsrpResponse, + expectedBroadcastVersion, expectedBroadcastTcpPort, expectedBroadcastPipeName); + + AssertResponse(unicastResponse, containsUnicastSsrpResponse, + expectedUnicastVersion, expectedUnicastTcpPort, expectedUnicastPipeName); + + static void AssertResponse(SqlDataSourceResponse response, bool containsResponse, + string expectedVersion, int expectedTcpPort, string? expectedPipeName) + { + Assert.True(containsResponse); + Assert.Equal("srv1", response.ServerName.ToString()); + Assert.Equal("MSSQLSERVER", response.InstanceName.ToString()); + Assert.Equal(expectedVersion, response.Version.ToString()); + + Assert.True(response.TcpEnabled); + Assert.Equal(expectedTcpPort, response.TcpPort); + + if (expectedPipeName is null) + { + Assert.False(response.NamedPipeEnabled); + } + else + { + Assert.True(response.NamedPipeEnabled); + Assert.Equal(expectedPipeName, response.NamedPipe.ToString()); + } + } + } +} 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 index 493a6996d5..d31539678a 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs @@ -35,7 +35,7 @@ internal static class SsrpPacketTestData /// One empty packet buffer, which should be successfully processed and contain zero responses. /// /// - /// + /// public static TheoryData> EmptyPacketBuffer => new(GeneratePacketBuffers([])); @@ -43,7 +43,7 @@ internal static class SsrpPacketTestData /// Various combinations of packet buffers containing normal SVR_RESP responses, all of which /// should be successfully processed. /// - /// + /// public static TheoryData, string, int, string?, string, int, string?> ValidSvrRespPacketBuffer { get @@ -149,7 +149,7 @@ internal static class SsrpPacketTestData /// Various combinations of packet buffers containing SVR_RESP (DAC) responses, all of which /// should be successfully processed. /// - /// + /// public static TheoryData, int> ValidSvrRespDacPacketBuffer { get @@ -302,7 +302,7 @@ public static TheoryData, int> ValidSvrRespDacPacketBuffe /// 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 @@ -323,7 +323,7 @@ public static TheoryData> InvalidClntUcastInstSvrRespPack /// 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 @@ -371,7 +371,7 @@ public static TheoryData> InvalidClntUcastInstSvrRespPack /// 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 @@ -569,7 +569,7 @@ public static TheoryData> InvalidRespDataPackets /// 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 From 8df8cd88ba0624f9f9c91b3ae583ecd7d7ecda90 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 28 Jul 2026 06:53:59 +0100 Subject: [PATCH 10/11] Follow-ups * Locate one reference to Encoding.UTF8 rather than to s_mbcsEncoding. * Indentation for conditional compilation. * Permit underscores in machine names. * Complete rename from DacResponseProcessorTest to DacResponseReaderTest. * XML documentation updates. --- .../Data/Sql/SqlDataSourceResponse.cs | 25 ++++++++++--------- .../Data/Sql/DacResponseReaderTest.cs | 2 +- .../Microsoft/Data/Sql/SsrpPacketTestData.cs | 14 +++++------ 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs index 345009d1c7..a26d0128fe 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs @@ -51,7 +51,7 @@ internal readonly ref struct SqlDataSourceResponse /// /// relates strictly to structural validity. It indicates solely /// that this component is the correctly-parsed product of a RESP_DATA string. It does not - /// indicate that the value is valid for the key (or safe for user consumption.)Please + /// indicate that the value is valid for the key (or safe for user consumption.) /// /// private readonly ref struct RespDataComponent @@ -165,7 +165,7 @@ public static bool TryParse(string respData, scoped ref int startPos, long maxVa // bytes in the string, and compare. if (maxValueLength != -1) { - int valueByteCount = Encoding.UTF8.GetByteCount(valueCandidate); + int valueByteCount = s_mbcsEncoding.GetByteCount(valueCandidate); if (valueByteCount > maxValueLength) { @@ -203,22 +203,22 @@ public bool TryGetBoolean(out bool value) public bool TryGetVersion([NotNullWhen(true)] out Version? value) { value = null; -#if NET + #if NET return Valid && Version.TryParse(Value, out value); -#else + #else return Valid && Version.TryParse(Value.ToString(), out value); -#endif + #endif } public bool TryGetUInt16(out ushort value) { value = 0; -#if NET + #if NET return Valid && ushort.TryParse(Value, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out value); -#else + #else return Valid && ushort.TryParse(Value.ToString(), System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out value); -#endif + #endif } public bool TryGetServerName(out ReadOnlySpan value) @@ -231,6 +231,7 @@ public bool TryGetServerName(out ReadOnlySpan value) { if (ch != '.' && ch != '-' + && ch != '_' && (!(ch >= '0' && ch <= '9')) && (!(ch >= 'A' && ch <= 'Z')) && (!(ch >= 'a' && ch <= 'z'))) @@ -424,11 +425,11 @@ private static bool TryParseRespData(scoped ref ReadOnlySequence sequence, try { -#if NET + #if NET decodedRespData = s_mbcsEncoding.GetString(sequence.Slice(0, responseSize)); -#else + #else decodedRespData = s_mbcsEncoding.GetString(sequence.Slice(0, responseSize).ToArray()); -#endif + #endif } catch (DecoderFallbackException) { @@ -442,7 +443,7 @@ private static bool TryParseRespData(scoped ref ReadOnlySequence sequence, || !serverNameToken.Key.Equals(ServerNameKey.AsSpan(), StringComparison.Ordinal) // The ServerName token must be a valid FQDN. // Note: This validation is stricter than called for in MC-SQLR. It is designed to - // ensure that server names only contain valid characters (".", "-", a-z, A-Z, 0-9). + // ensure that server names only contain valid characters (".", "-", "_", a-z, A-Z, 0-9). || !serverNameToken.TryGetServerName(out ReadOnlySpan serverName) || !RespDataComponent.TryParse(decodedRespData, ref currRespDataOffset, RespDataComponent.MaxInstanceNameLength, out RespDataComponent instanceNameToken) // The second token must be InstanceName. diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseReaderTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseReaderTest.cs index e1bf832113..805c005cb8 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseReaderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/DacResponseReaderTest.cs @@ -9,7 +9,7 @@ namespace Microsoft.Data.Sql.UnitTests; -public class DacResponseProcessorTest +public class DacResponseReaderTest { [Theory] [MemberData(nameof(SsrpPacketTestData.EmptyPacketBuffer), MemberType = typeof(SsrpPacketTestData), DisableDiscoveryEnumeration = true)] 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 index d31539678a..a18478605c 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs @@ -32,18 +32,18 @@ internal static class SsrpPacketTestData private const int ValidTcpPort5 = 1437; /// - /// One empty packet buffer, which should be successfully processed and contain zero responses. + /// One empty packet buffer, which should be successfully read 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. + /// should be successfully read. /// - /// + /// public static TheoryData, string, int, string?, string, int, string?> ValidSvrRespPacketBuffer { get @@ -147,9 +147,9 @@ internal static class SsrpPacketTestData /// /// Various combinations of packet buffers containing SVR_RESP (DAC) responses, all of which - /// should be successfully processed. + /// should be successfully read. /// - /// + /// public static TheoryData, int> ValidSvrRespDacPacketBuffer { get @@ -268,7 +268,7 @@ public static TheoryData, int> ValidSvrRespDacPacketBuffe /// /// Packet buffers containing nothing but invalid SVR_RESP (DAC) responses. /// - /// + /// public static TheoryData> InvalidSvrRespDacPackets => [ // Invalid header byte From 11185a54299fdc4ead685e627fbb03288658d8d5 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:15:15 +0100 Subject: [PATCH 11/11] Add additional test cases Also provide an early break if the BV_INFO protocol component is too short --- .../Data/Sql/SqlDataSourceResponse.cs | 2 +- .../Microsoft/Data/Sql/SsrpPacketTestData.cs | 38 ++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs index a26d0128fe..a0eda05964 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceResponse.cs @@ -559,7 +559,7 @@ private static bool TryParseRespProtocolData(string respData, ref int currPos, o if (!RespDataComponent.TryParse(respData, ref tempCurrPos, maxValueLength: -1, expectedTokensInValue: 5, out currentToken)) { - break; + return false; } } 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 index a18478605c..156c8ced25 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Sql/SsrpPacketTestData.cs @@ -69,7 +69,7 @@ internal static class SsrpPacketTestData 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}"))); + respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: false, ValidServerVersion, CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort4}"))); byte[] validPacket5 = FormatSvrRespMessage(ValidSvrRespHeader, respData: CreateRespData(ValidServerName, ValidInstanceName, isClustered: true, ValidServerVersion, smuggledProtocolParameters)); byte[] invalidPacket1 = FormatSvrRespMessage(ValidSvrRespHeader, @@ -377,9 +377,15 @@ public static TheoryData> InvalidRespDataPackets get { const string InvalidServerName = "sr\u0008v\u00001"; + const string InvalidInstanceName = "MSSQL\u0000SQSERVER"; string validTcpInfo = CreateProtocolParameters($"tcp;{ValidTcpPort1}"); return [ + // String does not decode into UTF8. + GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader, + // 0xC3, 0xA9 is é. Supply 45 bytes + [.. "éééééééééééééééééééééé"u8, 0xC3])), + // All keys lowercase GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader, CreateRespData(ValidServerName, @@ -466,6 +472,14 @@ public static TheoryData> InvalidRespDataPackets ValidServerVersion, validTcpInfo))), + // Instance name contains invalid characters + GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader, + CreateRespData(ValidServerName, + instanceName: InvalidInstanceName, + isClustered: true, + ValidServerVersion, + validTcpInfo))), + // Missing "IsClustered" GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader, CreateRespData(ValidServerName, @@ -557,10 +571,30 @@ public static TheoryData> InvalidRespDataPackets ValidServerVersion, CreateProtocolParameters(otherParameters: ";value")))), + // Valid protocol components appear with short values + GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader, + CreateRespData(ValidServerName, + ValidInstanceName, + isClustered: true, + ValidServerVersion, + CreateProtocolParameters(tcpInfo: $"tcp;{ValidTcpPort2}", bvInfo: "bv;first;second;third")))), + // Invalid PROTOCOLVERSION field value GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader, ValidRespDataDacResponseSize, - CreateRespData(protocolVersion: 0x02, ValidTcpPort2))) + CreateRespData(protocolVersion: 0x02, ValidTcpPort2))), + + // RESP_SIZE is correct, but too small for RESP_DATA to be valid (normal response) + GeneratePacketBuffers(FormatSvrRespMessage(ValidSvrRespHeader, + CreateRespData(ValidServerName, + ValidInstanceName, + isClustered: true, + ValidServerVersion, + protocolParameters: new string('a', 5), + omitServerName: true, + omitInstanceName: true, + omitIsClustered: true, + omitVersion: true))) ]; } }