diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
index 6246fc590d..1022ff06ac 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
@@ -188,6 +188,9 @@
Microsoft\Data\SqlClient\OnChangedEventHandler.cs
+
+ Microsoft\Data\SqlClient\Packet.cs
+
Microsoft\Data\SqlClient\ParameterPeekAheadValue.cs
@@ -581,6 +584,9 @@
Microsoft\Data\SqlClient\TdsParserStateObject.cs
+
+ Microsoft\Data\SqlClient\TdsParserStateObject.Multiplexer.cs
+
Microsoft\Data\SqlClient\TdsParserStaticMethods.cs
diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs
index f0ae13e6cd..05fb82f7e4 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs
@@ -3724,7 +3724,7 @@ private TdsOperationStatus TryNextResult(out bool more)
///
// user must call Read() to position on the first row
- override public bool Read()
+ public override bool Read()
{
if (_currentTask != null)
{
@@ -4564,9 +4564,10 @@ internal TdsOperationStatus TrySetMetaData(_SqlMetaDataSet metaData, bool moreIn
_metaDataConsumed = true;
if (_parser != null)
- { // There is a valid case where parser is null
- // Peek, and if row token present, set _hasRows true since there is a
- // row in the result
+ {
+ // There is a valid case where parser is null
+ // Peek, and if row token present, set _hasRows true since there is a
+ // row in the result
byte b;
TdsOperationStatus result = _stateObj.TryPeekByte(out b);
if (result != TdsOperationStatus.Done)
diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs
index e2d86dc210..7c10f0aa4f 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs
@@ -25,6 +25,10 @@ internal void PostReadAsyncForMars()
_pMarsPhysicalConObj.IncrementPendingCallbacks();
SessionHandle handle = _pMarsPhysicalConObj.SessionHandle;
+ // we do not need to consider partial packets when making this read because we
+ // expect this read to pend. a partial packet should not exist at setup of the
+ // parser
+ Debug.Assert(_physicalStateObj.PartialPacket==null);
temp = _pMarsPhysicalConObj.ReadAsync(handle, out error);
Debug.Assert(temp.Type == PacketHandle.NativePointerType, "unexpected packet type when requiring NativePointer");
diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs
index af920ed2a7..3f34ec4ded 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs
@@ -2047,11 +2047,19 @@ internal TdsOperationStatus TryRun(RunBehavior runBehavior, SqlCommand cmdHandle
if (!IsValidTdsToken(token))
{
- Debug.Fail($"unexpected token; token = {token,-2:X2}");
+#if DEBUG
+ string message = stateObj.DumpBuffer();
+ Debug.Fail(message);
+#endif
_state = TdsParserState.Broken;
_connHandler.BreakConnection();
SqlClientEventSource.Log.TryTraceEvent(" Potential multi-threaded misuse of connection, unexpected TDS token found {0}", ObjectID);
+#if DEBUG
+ throw new InvalidOperationException(message);
+#else
throw SQL.ParsingError();
+#endif
+
}
int tokenLength;
@@ -4143,6 +4151,7 @@ internal TdsOperationStatus TryProcessReturnValue(int length, TdsParserStateObje
{
return result;
}
+
byte len;
result = stateObj.TryReadByte(out len);
if (result != TdsOperationStatus.Done)
@@ -4540,7 +4549,6 @@ internal TdsOperationStatus TryProcessCollation(TdsParserStateObject stateObj, o
collation = null;
return result;
}
-
if (SqlCollation.Equals(_cachedCollation, info, sortId))
{
collation = _cachedCollation;
@@ -5263,7 +5271,7 @@ private TdsOperationStatus TryCommonProcessMetaData(TdsParserStateObject stateOb
{
// If the column is encrypted, we should have a valid cipherTable
if (cipherTable != null)
- {
+ {
result = TryProcessTceCryptoMetadata(stateObj, col, cipherTable, columnEncryptionSetting, isReturnValue: false);
if (result != TdsOperationStatus.Done)
{
diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs
index 4f655dc403..aa6986ca2d 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs
@@ -322,14 +322,22 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error)
stateObj.SendAttention(mustTakeWriteLock: true);
PacketHandle syncReadPacket = default;
+ bool readFromNetwork = true;
RuntimeHelpers.PrepareConstrainedRegions();
bool shouldDecrement = false;
try
{
Interlocked.Increment(ref _readingCount);
shouldDecrement = true;
-
- syncReadPacket = ReadSyncOverAsync(stateObj.GetTimeoutRemaining(), out error);
+ readFromNetwork = !PartialPacketContainsCompletePacket();
+ if (readFromNetwork)
+ {
+ syncReadPacket = ReadSyncOverAsync(stateObj.GetTimeoutRemaining(), out error);
+ }
+ else
+ {
+ error = TdsEnums.SNI_SUCCESS;
+ }
Interlocked.Decrement(ref _readingCount);
shouldDecrement = false;
@@ -342,7 +350,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error)
}
else
{
- Debug.Assert(!IsValidPacket(syncReadPacket), "unexpected syncReadPacket without corresponding SNIPacketRelease");
+ Debug.Assert(!readFromNetwork || !IsValidPacket(syncReadPacket), "unexpected syncReadPacket without corresponding SNIPacketRelease");
fail = true; // Subsequent read failed, time to give up.
}
}
@@ -353,7 +361,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error)
Interlocked.Decrement(ref _readingCount);
}
- if (!IsPacketEmpty(syncReadPacket))
+ if (readFromNetwork && !IsPacketEmpty(syncReadPacket))
{
ReleasePacket(syncReadPacket);
}
@@ -393,60 +401,9 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error)
AssertValidState();
}
- public void ProcessSniPacket(PacketHandle packet, uint error)
+ private uint GetSniPacket(PacketHandle packet, ref uint dataSize)
{
- if (error != 0)
- {
- if ((_parser.State == TdsParserState.Closed) || (_parser.State == TdsParserState.Broken))
- {
- // Do nothing with callback if closed or broken and error not 0 - callback can occur
- // after connection has been closed. PROBLEM IN NETLIB - DESIGN FLAW.
- return;
- }
-
- AddError(_parser.ProcessSNIError(this));
- AssertValidState();
- }
- else
- {
- uint dataSize = 0;
-
- uint getDataError = SNIPacketGetData(packet, _inBuff, ref dataSize);
-
- if (getDataError == TdsEnums.SNI_SUCCESS)
- {
- if (_inBuff.Length < dataSize)
- {
- Debug.Assert(true, "Unexpected dataSize on Read");
- throw SQL.InvalidInternalPacketSize(StringsHelper.GetString(Strings.SqlMisc_InvalidArraySizeMessage));
- }
-
- _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks;
- _inBytesRead = (int)dataSize;
- _inBytesUsed = 0;
-
- if (_snapshot != null)
- {
- _snapshot.AppendPacketData(_inBuff, _inBytesRead);
- if (_snapshotReplay)
- {
- _snapshot.MoveNext();
-#if DEBUG
- _snapshot.AssertCurrent();
-#endif
- }
- }
-
- SniReadStatisticsAndTracing();
- SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParser.ReadNetworkPacketAsyncCallback | INFO | ADV | State Object Id {0}, Packet read. In Buffer: {1}, In Bytes Read: {2}", ObjectID, _inBuff, _inBytesRead);
-
- AssertValidState();
- }
- else
- {
- throw SQL.ParsingError(ParsingErrorState.ProcessSniPacketFailed);
- }
- }
+ return SNIPacketGetData(packet, _inBuff, ref dataSize);
}
private void ChangeNetworkPacketTimeout(int dueTime, int period)
@@ -535,7 +492,7 @@ public void ReadAsyncCallback(IntPtr key, PacketHandle packet, uint error)
bool processFinallyBlock = true;
try
{
- Debug.Assert(CheckPacket(packet, source) && source != null, "AsyncResult null on callback");
+ Debug.Assert((packet.Type == 0 && PartialPacketContainsCompletePacket()) || (CheckPacket(packet, source) && source != null), "AsyncResult null on callback");
if (_parser.MARSOn)
{
@@ -547,7 +504,7 @@ public void ReadAsyncCallback(IntPtr key, PacketHandle packet, uint error)
// The timer thread may be unreliable under high contention scenarios. It cannot be
// assumed that the timeout has happened on the timer thread callback. Check the timeout
- // synchrnously and then call OnTimeoutSync to force an atomic change of state.
+ // synchronously and then call OnTimeoutSync to force an atomic change of state.
if (TimeoutHasExpired)
{
OnTimeoutSync(asyncClose: true);
@@ -1633,7 +1590,7 @@ internal void AssertStateIsClean()
if ((parser != null) && (parser.State != TdsParserState.Closed) && (parser.State != TdsParserState.Broken))
{
// Async reads
- Debug.Assert(_snapshot == null && !_snapshotReplay, "StateObj has leftover snapshot state");
+ Debug.Assert(_snapshot == null && _snapshotStatus == SnapshotStatus.NotActive, "StateObj has leftover snapshot state");
Debug.Assert(!_asyncReadWithoutSnapshot, "StateObj has AsyncReadWithoutSnapshot still enabled");
Debug.Assert(_executionContext == null, "StateObj has a stored execution context from an async read");
// Async writes
diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
index 591977ef2d..feecd6f44f 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
@@ -391,6 +391,9 @@
Microsoft\Data\SqlClient\OnChangedEventHandler.cs
+
+ Microsoft\Data\SqlClient\Packet.cs
+
Microsoft\Data\SqlClient\ParameterPeekAheadValue.cs
@@ -772,6 +775,9 @@
Microsoft\Data\SqlClient\TdsParserStateObject.cs
+
+ Microsoft\Data\SqlClient\TdsParserStateObject.Multiplexer.cs
+
Microsoft\Data\SqlClient\TdsParserStaticMethods.cs
diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs
index e45765c0a4..a44cabd134 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs
@@ -4364,6 +4364,7 @@ internal TdsOperationStatus TryProcessReturnValue(int length,
return result;
}
}
+
byte len;
result = stateObj.TryReadByte(out len);
if (result != TdsOperationStatus.Done)
diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs
index 0ce58d120a..343778cb4e 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs
@@ -453,14 +453,22 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error)
stateObj.SendAttention(mustTakeWriteLock: true);
PacketHandle syncReadPacket = default;
+ bool readFromNetwork = true;
RuntimeHelpers.PrepareConstrainedRegions();
bool shouldDecrement = false;
try
{
Interlocked.Increment(ref _readingCount);
shouldDecrement = true;
-
- syncReadPacket = ReadSyncOverAsync(stateObj.GetTimeoutRemaining(), out error);
+ readFromNetwork = !PartialPacketContainsCompletePacket();
+ if (readFromNetwork)
+ {
+ syncReadPacket = ReadSyncOverAsync(stateObj.GetTimeoutRemaining(), out error);
+ }
+ else
+ {
+ error = TdsEnums.SNI_SUCCESS;
+ }
Interlocked.Decrement(ref _readingCount);
shouldDecrement = false;
@@ -473,7 +481,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error)
}
else
{
- Debug.Assert(!IsValidPacket(syncReadPacket), "unexpected syncReadPacket without corresponding SNIPacketRelease");
+ Debug.Assert(!readFromNetwork || !IsValidPacket(syncReadPacket), "unexpected syncReadPacket without corresponding SNIPacketRelease");
fail = true; // Subsequent read failed, time to give up.
}
}
@@ -484,7 +492,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error)
Interlocked.Decrement(ref _readingCount);
}
- if (!IsPacketEmpty(syncReadPacket))
+ if (readFromNetwork && !IsPacketEmpty(syncReadPacket))
{
// Be sure to release packet, otherwise it will be leaked by native.
ReleasePacket(syncReadPacket);
@@ -525,60 +533,9 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error)
AssertValidState();
}
- public void ProcessSniPacket(PacketHandle packet, uint error)
+ private uint GetSniPacket(PacketHandle packet, ref uint dataSize)
{
- if (error != 0)
- {
- if ((_parser.State == TdsParserState.Closed) || (_parser.State == TdsParserState.Broken))
- {
- // Do nothing with callback if closed or broken and error not 0 - callback can occur
- // after connection has been closed. PROBLEM IN NETLIB - DESIGN FLAW.
- return;
- }
-
- AddError(_parser.ProcessSNIError(this));
- AssertValidState();
- }
- else
- {
- uint dataSize = 0;
-
- uint getDataError = SniNativeWrapper.SNIPacketGetData(packet, _inBuff, ref dataSize);
-
- if (getDataError == TdsEnums.SNI_SUCCESS)
- {
- if (_inBuff.Length < dataSize)
- {
- Debug.Assert(true, "Unexpected dataSize on Read");
- throw SQL.InvalidInternalPacketSize(StringsHelper.GetString(Strings.SqlMisc_InvalidArraySizeMessage));
- }
-
- _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks;
- _inBytesRead = (int)dataSize;
- _inBytesUsed = 0;
-
- if (_snapshot != null)
- {
- _snapshot.AppendPacketData(_inBuff, _inBytesRead);
- if (_snapshotReplay)
- {
- _snapshot.MoveNext();
-#if DEBUG
- _snapshot.AssertCurrent();
-#endif
- }
- }
-
- SniReadStatisticsAndTracing();
- SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParser.ReadNetworkPacketAsyncCallback | INFO | ADV | State Object Id {0}, Packet read. In Buffer: {1}, In Bytes Read: {2}", ObjectID, _inBuff, _inBytesRead);
-
- AssertValidState();
- }
- else
- {
- throw SQL.ParsingError(ParsingErrorState.ProcessSniPacketFailed);
- }
- }
+ return SniNativeWrapper.SNIPacketGetData(packet, _inBuff, ref dataSize);
}
private void ChangeNetworkPacketTimeout(int dueTime, int period)
@@ -1774,7 +1731,7 @@ internal void AssertStateIsClean()
if ((parser != null) && (parser.State != TdsParserState.Closed) && (parser.State != TdsParserState.Broken))
{
// Async reads
- Debug.Assert(_snapshot == null && !_snapshotReplay, "StateObj has leftover snapshot state");
+ Debug.Assert(_snapshot == null && _snapshotStatus == SnapshotStatus.NotActive, "StateObj has leftover snapshot state");
Debug.Assert(!_asyncReadWithoutSnapshot, "StateObj has AsyncReadWithoutSnapshot still enabled");
Debug.Assert(_executionContext == null, "StateObj has a stored execution context from an async read");
// Async writes
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs
index 9ea289f5b7..b66154a2ae 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs
@@ -20,6 +20,7 @@ private enum Tristate : byte
internal const string SuppressInsecureTLSWarningString = @"Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning";
internal const string UseMinimumLoginTimeoutString = @"Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin";
internal const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour";
+ internal const string UseCompatibilityProcessSniString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni";
// this field is accessed through reflection in tests and should not be renamed or have the type changed without refactoring NullRow related tests
private static Tristate s_legacyRowVersionNullBehavior;
@@ -28,6 +29,7 @@ private enum Tristate : byte
private static Tristate s_useMinimumLoginTimeout;
// this field is accessed through reflection in Microsoft.Data.SqlClient.Tests.SqlParameterTests and should not be renamed or have the type changed without refactoring related tests
private static Tristate s_legacyVarTimeZeroScaleBehaviour;
+ private static Tristate s_useCompatProcessSni;
#if NET
static LocalAppContextSwitches()
@@ -83,6 +85,24 @@ public static bool DisableTNIRByDefault
}
}
#endif
+ public static bool UseCompatibilityProcessSni
+ {
+ get
+ {
+ if (s_useCompatProcessSni == Tristate.NotInitialized)
+ {
+ if (AppContext.TryGetSwitch(UseCompatibilityProcessSniString, out bool returnedValue) && returnedValue)
+ {
+ s_useCompatProcessSni = Tristate.True;
+ }
+ else
+ {
+ s_useCompatProcessSni = Tristate.False;
+ }
+ }
+ return s_useCompatProcessSni == Tristate.True;
+ }
+ }
///
/// When using Encrypt=false in the connection string, a security warning is output to the console if the TLS version is 1.2 or lower.
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Packet.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Packet.cs
new file mode 100644
index 0000000000..b81270bf08
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Packet.cs
@@ -0,0 +1,189 @@
+// 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.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.Data.SqlClient
+{
+ ///
+ /// Contains a buffer for a partial or full packet and methods to get information about the status of
+ /// the packet that the buffer represents.
+ /// This class is used to contain partial packet data and helps ensure that the packet data is completely
+ /// received before a full packet is made available to the rest of the library
+ ///
+ internal sealed partial class Packet
+ {
+ public const int UnknownDataLength = -1;
+
+ private int _dataLength;
+ private int _totalLength;
+ private byte[] _buffer;
+
+ public Packet()
+ {
+ _dataLength = UnknownDataLength;
+ }
+
+ ///
+ /// If the packet data has enough bytes available to determine the amount of data that should be present
+ /// in the packet then this property will be set to the count of data bytes in the packet,
+ /// otherwise this will be -1
+ ///
+ public int DataLength
+ {
+ get
+ {
+ CheckDisposed();
+ return _dataLength;
+ }
+ set
+ {
+ CheckDisposed();
+ _dataLength = value;
+ }
+ }
+
+ ///
+ /// A byte array containing bytes of data
+ ///
+ public byte[] Buffer
+ {
+ get
+ {
+ CheckDisposed();
+ return _buffer;
+ }
+ set
+ {
+ CheckDisposed();
+ _buffer = value;
+ }
+ }
+
+ ///
+ /// The total count of bytes currently in the array including the tds header bytes
+ ///
+ public int CurrentLength
+ {
+ get
+ {
+ CheckDisposed();
+ return _totalLength;
+ }
+ set
+ {
+ CheckDisposed();
+ _totalLength = value;
+ }
+ }
+
+ ///
+ /// If the packet data has enough bytes available to determine the length amount of data that should be present
+ /// in the packet then this property will return the count of data bytes that are expected to be in the packet.
+ /// If there are not enough bytes to determine the data byte count then this property will throw an exception.
+ ///
+ /// Call to check if there will be a value before using this property.
+ ///
+ public int RequiredLength
+ {
+ get
+ {
+ CheckDisposed();
+ if (!HasDataLength)
+ {
+ throw new InvalidOperationException($"cannot get {nameof(RequiredLength)} when {nameof(HasDataLength)} is false");
+ }
+ return TdsEnums.HEADER_LEN + _dataLength;
+ }
+ }
+
+ ///
+ /// returns a boolean value indicating if there are enough total bytes available in the to read the tds header
+ ///
+ public bool HasHeader => _totalLength >= TdsEnums.HEADER_LEN;
+
+ ///
+ /// returns a boolean value indicating if the value has been set.
+ ///
+ public bool HasDataLength => _dataLength >= 0;
+
+ ///
+ /// returns a boolean value indicating whether the contains enough
+ /// data for a valid tds header, has a set and that the
+ /// is greater than or equal to the + tds header length.
+ ///
+ public bool ContainsCompletePacket => _dataLength != UnknownDataLength && (TdsEnums.HEADER_LEN + _dataLength) <= _totalLength;
+
+ ///
+ /// returns a containing the first 8 bytes of the array which will
+ /// contain the TDS header bytes. This can be passed to static functions on to extract information from the
+ /// tds packet header.
+ /// Call before using this function.
+ ///
+ ///
+ public ReadOnlySpan GetHeaderSpan() => _buffer.AsSpan(0, TdsEnums.HEADER_LEN);
+
+ [Conditional("DEBUG")]
+ internal void CheckDisposed() => CheckDisposedImpl();
+
+ [Conditional("DEBUG")]
+ internal void SetCreatedBy(int creator) => SetCreatedByImpl(creator);
+
+ partial void SetCreatedByImpl(int creator);
+
+ partial void CheckDisposedImpl();
+
+ public static void ThrowDisposed()
+ {
+ throw new ObjectDisposedException(nameof(Packet));
+ }
+
+ internal static byte GetStatusFromHeader(ReadOnlySpan header) => header[1];
+
+ internal static int GetDataLengthFromHeader(ReadOnlySpan header)
+ {
+ return (header[TdsEnums.HEADER_LEN_FIELD_OFFSET] << 8 | header[TdsEnums.HEADER_LEN_FIELD_OFFSET + 1]) - TdsEnums.HEADER_LEN;
+ }
+ internal static int GetSpidFromHeader(ReadOnlySpan header)
+ {
+ return (header[TdsEnums.SPID_OFFSET] << 8 | header[TdsEnums.SPID_OFFSET + 1]);
+ }
+ internal static int GetIDFromHeader(ReadOnlySpan header)
+ {
+ return header[TdsEnums.HEADER_LEN_FIELD_OFFSET + 4];
+ }
+
+ internal static int GetDataLengthFromHeader(Packet packet) => GetDataLengthFromHeader(packet.GetHeaderSpan());
+
+ internal static bool GetIsEOMFromHeader(ReadOnlySpan header) => GetStatusFromHeader(header) == 1;
+ }
+
+#if DEBUG
+ internal sealed partial class Packet
+ {
+ private int _createdBy;
+ private bool _disposed;
+
+ public int CreatedBy => _createdBy;
+
+ [Conditional("DEBUG")]
+ public void Dispose()
+ {
+ _disposed = true;
+ }
+
+ partial void SetCreatedByImpl(int creator) => _createdBy = creator;
+
+ partial void CheckDisposedImpl()
+ {
+ if (_disposed)
+ {
+ ThrowDisposed();
+ }
+ }
+ }
+#endif
+}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.Multiplexer.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.Multiplexer.cs
new file mode 100644
index 0000000000..2bb72e9bf2
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.Multiplexer.cs
@@ -0,0 +1,559 @@
+// 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.Diagnostics;
+
+namespace Microsoft.Data.SqlClient
+{
+#if NETFRAMEWORK
+ using PacketHandle = IntPtr;
+#endif
+ partial class TdsParserStateObject
+ {
+ private Packet _partialPacket;
+ internal Packet PartialPacket => _partialPacket;
+
+ public void ProcessSniPacket(PacketHandle packet, uint error)
+ {
+ if (LocalAppContextSwitches.UseCompatibilityProcessSni)
+ {
+ ProcessSniPacketCompat(packet, error);
+ return;
+ }
+
+ if (error != 0)
+ {
+ if ((_parser.State == TdsParserState.Closed) || (_parser.State == TdsParserState.Broken))
+ {
+ // Do nothing with callback if closed or broken and error not 0 - callback can occur
+ // after connection has been closed. PROBLEM IN NETLIB - DESIGN FLAW.
+ return;
+ }
+
+ AddError(_parser.ProcessSNIError(this));
+ AssertValidState();
+ }
+ else
+ {
+ uint dataSize = 0;
+ bool usedPartialPacket = false;
+ uint getDataError = 0;
+
+ if (PartialPacketContainsCompletePacket())
+ {
+ Packet partialPacket = _partialPacket;
+ SetBuffer(partialPacket.Buffer, 0, partialPacket.CurrentLength);
+ ClearPartialPacket();
+ getDataError = TdsEnums.SNI_SUCCESS;
+ usedPartialPacket = true;
+ }
+ else
+ {
+ if (_inBytesRead != 0)
+ {
+ SetBuffer(new byte[_inBuff.Length], 0, 0);
+ }
+ getDataError = GetSniPacket(packet, ref dataSize);
+ }
+
+ if (getDataError == TdsEnums.SNI_SUCCESS)
+ {
+ if (_inBuff.Length < dataSize)
+ {
+ Debug.Assert(true, "Unexpected dataSize on Read");
+ throw SQL.InvalidInternalPacketSize(StringsHelper.GetString(Strings.SqlMisc_InvalidArraySizeMessage));
+ }
+
+ if (!usedPartialPacket)
+ {
+ _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks;
+
+ SetBuffer(_inBuff, 0, (int)dataSize);
+ }
+
+ bool recurse = false;
+ bool appended = false;
+ do
+ {
+ if (recurse && appended)
+ {
+ SetBuffer(new byte[_inBuff.Length], 0, 0);
+ appended = false;
+ }
+ MultiplexPackets(
+ _inBuff, _inBytesUsed, _inBytesRead,
+ PartialPacket,
+ out int newDataOffset,
+ out int newDataLength,
+ out Packet remainderPacket,
+ out bool consumeInputDirectly,
+ out bool consumePartialPacket,
+ out bool remainderPacketProduced,
+ out recurse
+ );
+ bool bufferIsPartialCompleted = false;
+
+ // if a partial packet was reconstructed it must be handled first
+ if (consumePartialPacket)
+ {
+ if (_snapshot != null)
+ {
+ _snapshot.AppendPacketData(PartialPacket.Buffer, PartialPacket.CurrentLength);
+ SetBuffer(new byte[_inBuff.Length], 0, 0);
+ appended = true;
+ }
+ else
+ {
+ SetBuffer(PartialPacket.Buffer, 0, PartialPacket.CurrentLength);
+
+ }
+ bufferIsPartialCompleted = true;
+ ClearPartialPacket();
+ }
+
+ // if the remaining data can be processed directly it must be second
+ if (consumeInputDirectly)
+ {
+ // if some data was taken from the new packet adjust the counters
+ if (dataSize != newDataLength || 0 != newDataOffset)
+ {
+ SetBuffer(_inBuff, newDataOffset, newDataLength);
+ }
+
+ if (_snapshot != null)
+ {
+ _snapshot.AppendPacketData(_inBuff, _inBytesRead);
+ // if we SetBuffer here to clear the packet buffer we will break the attention handling which relies
+ // on the attention containing packet remaining in the active buffer even if we're appending to the
+ // snapshot so we will have to use the appended variable to prevent the same buffer being added again
+ //// SetBuffer(new byte[_inBuff.Length], 0, 0);
+ appended = true;
+ }
+ else
+ {
+ SetBuffer(_inBuff, 0, _inBytesRead);
+ }
+ bufferIsPartialCompleted = true;
+ }
+ else
+ {
+ // whatever is in the input buffer should not be directly consumed
+ // and is contained in the partial or remainder packets so make sure
+ // we don't process it
+ if (!bufferIsPartialCompleted)
+ {
+ SetBuffer(_inBuff, 0, 0);
+ }
+ }
+
+ // if there is a remainder it must be last
+ if (remainderPacketProduced)
+ {
+ SetPartialPacket(remainderPacket);
+ if (!bufferIsPartialCompleted)
+ {
+ // we are keeping the partial packet buffer so replace it with a new one
+ // unless we have already set the buffer to the partial packet buffer
+ SetBuffer(new byte[_inBuff.Length], 0, 0);
+ }
+ }
+
+ } while (recurse && _snapshot != null);
+
+ if (_snapshot != null)
+ {
+ if (_snapshotStatus != SnapshotStatus.NotActive && appended)
+ {
+ _snapshot.MoveNext();
+ }
+ }
+
+ SniReadStatisticsAndTracing();
+ SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParser.ReadNetworkPacketAsyncCallback | INFO | ADV | State Object Id {0}, Packet read. In Buffer {1}, In Bytes Read: {2}", ObjectID, _inBuff, (ushort)_inBytesRead);
+
+ AssertValidState();
+ }
+ else
+ {
+ throw SQL.ParsingError(ParsingErrorState.ProcessSniPacketFailed);
+ }
+ }
+ }
+
+ private void SetPartialPacket(Packet packet)
+ {
+ if (_partialPacket != null && packet != null)
+ {
+ throw new InvalidOperationException("partial packet cannot be non-null when setting to non=null");
+ }
+ _partialPacket = packet;
+ }
+
+ private void ClearPartialPacket()
+ {
+ Packet partialPacket = _partialPacket;
+ _partialPacket = null;
+#if DEBUG
+ if (partialPacket != null)
+ {
+ partialPacket.Dispose();
+ }
+#endif
+ }
+
+ // this check is used in two places that must be identical so it is
+ // extracted into a method, do not inline this method
+ internal bool PartialPacketContainsCompletePacket()
+ {
+ Packet partialPacket = _partialPacket;
+ return partialPacket != null && partialPacket.ContainsCompletePacket;
+ }
+
+ private static void MultiplexPackets(
+ byte[] dataBuffer, int dataOffset, int dataLength,
+ Packet partialPacket,
+ out int newDataOffset,
+ out int newDataLength,
+ out Packet remainderPacket,
+ out bool consumeInputDirectly,
+ out bool consumePartialPacket,
+ out bool createdRemainderPacket,
+ out bool recurse
+ )
+ {
+ Debug.Assert(dataBuffer != null);
+
+ ReadOnlySpan data = dataBuffer.AsSpan(dataOffset, dataLength);
+ remainderPacket = null;
+ consumeInputDirectly = false;
+ consumePartialPacket = false;
+ createdRemainderPacket = false;
+ recurse = false;
+
+ newDataLength = dataLength;
+ newDataOffset = dataOffset;
+
+ int bytesConsumed = 0;
+
+ if (partialPacket != null)
+ {
+ if (!partialPacket.HasDataLength)
+ {
+ // we need to get enough bytes to read the packet header
+ int headerBytesNeeded = Math.Max(0, TdsEnums.HEADER_LEN - partialPacket.CurrentLength);
+ if (headerBytesNeeded > 0)
+ {
+ int headerBytesAvailable = Math.Min(data.Length, headerBytesNeeded);
+
+ Span headerTarget = partialPacket.Buffer.AsSpan(partialPacket.CurrentLength, headerBytesAvailable);
+ ReadOnlySpan headerSource = data.Slice(0, headerBytesAvailable);
+ headerSource.CopyTo(headerTarget);
+
+ partialPacket.CurrentLength = partialPacket.CurrentLength + headerBytesAvailable;
+ bytesConsumed += headerBytesAvailable;
+ data = data.Slice(headerBytesAvailable);
+ }
+ if (partialPacket.HasHeader)
+ {
+ partialPacket.DataLength = Packet.GetDataLengthFromHeader(partialPacket);
+ }
+ }
+
+ if (partialPacket.HasDataLength)
+ {
+ if (partialPacket.CurrentLength < partialPacket.RequiredLength)
+ {
+ // the packet length is known so take as much data as possible from the incoming
+ // data to try and complete the packet
+
+ int payloadBytesNeeded = partialPacket.DataLength - (partialPacket.CurrentLength - TdsEnums.HEADER_LEN);
+ int payloadBytesAvailable = Math.Min(data.Length, payloadBytesNeeded);
+
+ ReadOnlySpan payloadSource = data.Slice(0, payloadBytesAvailable);
+ Span payloadTarget = partialPacket.Buffer.AsSpan(partialPacket.CurrentLength, payloadBytesAvailable);
+ payloadSource.CopyTo(payloadTarget);
+
+ partialPacket.CurrentLength = partialPacket.CurrentLength + payloadBytesAvailable;
+ bytesConsumed += payloadBytesAvailable;
+ data = data.Slice(payloadBytesAvailable);
+ }
+ else if (partialPacket.CurrentLength > partialPacket.RequiredLength)
+ {
+ // the partial packet contains a complete packet of data and also contains
+ // data from a following packet
+
+ // the TDS spec requires that all packets be of the defined packet size apart from
+ // the last packet of a response. This means that is should not possible to have more than
+ // 2 packet fragments in a single buffer like this:
+ // - first packet caused the partial
+ // - second packet is the one we have just unpacked
+ // - third packet is the extra data we have found
+ // however, due to the timing of cancellation it is possible that a response token stream
+ // has ended before an attention message response is sent leaving us with a short final
+ // packet and an additional short cancel packet following it
+
+ // this should only happen when the caller is trying to consume the partial packet
+ // and does not have new input data
+
+ int remainderLength = partialPacket.CurrentLength - partialPacket.RequiredLength;
+
+ partialPacket.CurrentLength = partialPacket.RequiredLength;
+
+ remainderPacket = new Packet
+ {
+ Buffer = new byte[dataBuffer.Length],
+ CurrentLength = remainderLength,
+ };
+ remainderPacket.SetCreatedBy(1);
+
+ ReadOnlySpan remainderSource = partialPacket.Buffer.AsSpan(TdsEnums.HEADER_LEN + partialPacket.DataLength, remainderLength);
+ Span remainderTarget = remainderPacket.Buffer.AsSpan(0, remainderLength);
+ remainderSource.CopyTo(remainderTarget);
+
+ createdRemainderPacket = true;
+
+ recurse = SetupRemainderPacket(remainderPacket);
+ }
+
+ if (partialPacket.CurrentLength == partialPacket.RequiredLength)
+ {
+ // partial packet has been completed
+ consumePartialPacket = true;
+ }
+ }
+
+ if (bytesConsumed > 0)
+ {
+ if (data.Length > 0)
+ {
+ // some data has been taken from the buffer and put into the partial
+ // packet buffer. We have data left so move the data we have to the
+ // start of the buffer so we can pass the buffer back as zero based
+ // to the caller avoiding offset calculations in the rest of this method
+ Buffer.BlockCopy(
+ dataBuffer, dataOffset + bytesConsumed, // from
+ dataBuffer, dataOffset, // to
+ dataLength - bytesConsumed // for
+ );
+#if DEBUG
+ // for debugging purposes fill the removed data area with an easily
+ // recognisable pattern so we can see if it is misused
+ Span removed = dataBuffer.AsSpan(dataOffset + (dataLength - bytesConsumed), bytesConsumed);
+ removed.Fill(0xFF);
+#endif
+
+ // then recreate the data span so that we're looking at the data
+ // that has been moved
+ data = dataBuffer.AsSpan(dataOffset, dataLength - bytesConsumed);
+ }
+
+ newDataLength = dataLength - bytesConsumed;
+ }
+ }
+
+ // partial packet handling should not make decisions about consuming the input buffer
+ Debug.Assert(!consumeInputDirectly);
+ // partial packet handling may only create a remainder packet when it is trying to consume the partial packet and has no incoming data
+ Debug.Assert(!createdRemainderPacket || data.Length == 0);
+
+ if (data.Length > 0)
+ {
+ if (data.Length >= TdsEnums.HEADER_LEN)
+ {
+ // we have enough bytes to read the packet header and see how
+ // much data we are expecting it to contain
+ int packetDataLength = Packet.GetDataLengthFromHeader(data);
+
+ if (data.Length == TdsEnums.HEADER_LEN + packetDataLength)
+ {
+ if (!consumePartialPacket)
+ {
+ // we can tell the caller that they should directly consume the data in
+ // the input buffer, this is the happy path
+ consumeInputDirectly = true;
+ }
+ else
+ {
+ // we took some data from the input to reconstruct the partial packet
+ // so we can't tell the caller to directly consume the packet in the
+ // input buffer, we need to construct a new remainder packet and then
+ // tell them to consume it
+ remainderPacket = new Packet
+ {
+ Buffer = dataBuffer,
+ CurrentLength = data.Length
+ };
+ remainderPacket.SetCreatedBy(2);
+ createdRemainderPacket = true;
+ recurse = SetupRemainderPacket(remainderPacket);
+ }
+ }
+ else if (data.Length < TdsEnums.HEADER_LEN + packetDataLength)
+ {
+ // an incomplete packet so create a remainder packet to pass back
+ remainderPacket = new Packet
+ {
+ Buffer = dataBuffer,
+ DataLength = packetDataLength,
+ CurrentLength = data.Length
+ };
+ remainderPacket.SetCreatedBy(3);
+ createdRemainderPacket = true;
+ recurse = SetupRemainderPacket(remainderPacket);
+ }
+ else // implied: current length > required length
+ {
+ // more data than required so need to split it out, but we can't do that
+ // here so we need to tell the caller to take the remainder packet and then
+ // call this function again
+ if (consumePartialPacket)
+ {
+ // we are already telling the caller to consume the partial packet so we
+ // can't tell them it to also consume the data in the buffer directly
+ // so create a remainder packet and pass it back.
+ remainderPacket = new Packet
+ {
+ Buffer = new byte[dataBuffer.Length],
+ CurrentLength = data.Length
+ };
+ remainderPacket.SetCreatedBy(4);
+ ReadOnlySpan remainderSource = data;
+ Span remainderTarget = remainderPacket.Buffer.AsSpan(0, remainderPacket.CurrentLength);
+ remainderSource.CopyTo(remainderTarget);
+
+ createdRemainderPacket = true;
+
+ recurse = SetupRemainderPacket(remainderPacket);
+ }
+ else
+ {
+ newDataLength = TdsEnums.HEADER_LEN + packetDataLength;
+ int remainderLength = data.Length - (TdsEnums.HEADER_LEN + packetDataLength);
+ remainderPacket = new Packet
+ {
+ Buffer = new byte[dataBuffer.Length],
+ CurrentLength = remainderLength
+ };
+ remainderPacket.SetCreatedBy(5);
+
+ ReadOnlySpan remainderSource = data.Slice(TdsEnums.HEADER_LEN + packetDataLength);
+ Span remainderTarget = remainderPacket.Buffer.AsSpan(0, remainderLength);
+ remainderSource.CopyTo(remainderTarget);
+#if DEBUG
+ // for debugging purposes fill the removed data area with an easily
+ // recognisable pattern so we can see if it is misused
+ Span removed = dataBuffer.AsSpan(TdsEnums.HEADER_LEN + packetDataLength, remainderLength);
+ removed.Fill(0xFF);
+#endif
+ createdRemainderPacket = true;
+ recurse = SetupRemainderPacket(remainderPacket);
+
+ consumeInputDirectly = true;
+ }
+ }
+ }
+ else
+ {
+ // either:
+ // 1) we took some data from the input to reconstruct the partial packet
+ // 2) there was less than a single packet header of data received
+ // in both cases we can't tell the caller to directly consume the packet
+ // in the input buffer, we need to construct a new remainder packet with
+ // the incomplete data and let the caller deal with it
+ remainderPacket = new Packet
+ {
+ Buffer = dataBuffer,
+ CurrentLength = data.Length
+ };
+ remainderPacket.SetCreatedBy(6);
+ createdRemainderPacket = true;
+ recurse = SetupRemainderPacket(remainderPacket);
+ }
+ }
+
+ if (consumePartialPacket && consumeInputDirectly)
+ {
+ throw new InvalidOperationException($"MultiplexPackets cannot return both {nameof(consumePartialPacket)} and {nameof(consumeInputDirectly)}");
+ }
+ }
+
+ private static bool SetupRemainderPacket(Packet packet)
+ {
+ Debug.Assert(packet != null);
+ bool containsFullPacket = false;
+ if (packet.HasHeader)
+ {
+ packet.DataLength = Packet.GetDataLengthFromHeader(packet);
+ if (packet.HasDataLength && packet.CurrentLength >= packet.RequiredLength)
+ {
+ containsFullPacket = true;
+ }
+ }
+
+ return containsFullPacket;
+ }
+
+
+ public void ProcessSniPacketCompat(PacketHandle packet, uint error)
+ {
+ if (error != 0)
+ {
+ if ((_parser.State == TdsParserState.Closed) || (_parser.State == TdsParserState.Broken))
+ {
+ // Do nothing with callback if closed or broken and error not 0 - callback can occur
+ // after connection has been closed. PROBLEM IN NETLIB - DESIGN FLAW.
+ return;
+ }
+
+ AddError(_parser.ProcessSNIError(this));
+ AssertValidState();
+ }
+ else
+ {
+ uint dataSize = 0;
+
+ uint getDataError =
+#if NETFRAMEWORK
+ SniNativeWrapper.
+#endif
+ SNIPacketGetData(packet, _inBuff, ref dataSize);
+
+ if (getDataError == TdsEnums.SNI_SUCCESS)
+ {
+ if (_inBuff.Length < dataSize)
+ {
+ Debug.Assert(true, "Unexpected dataSize on Read");
+ throw SQL.InvalidInternalPacketSize(StringsHelper.GetString(Strings.SqlMisc_InvalidArraySizeMessage));
+ }
+
+ _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks;
+ _inBytesRead = (int)dataSize;
+ _inBytesUsed = 0;
+
+ if (_snapshot != null)
+ {
+ _snapshot.AppendPacketData(_inBuff, _inBytesRead);
+ if (_snapshotStatus != SnapshotStatus.NotActive)
+ {
+ _snapshot.MoveNext();
+#if DEBUG
+ _snapshot.AssertCurrent();
+#endif
+ }
+ }
+
+ SniReadStatisticsAndTracing();
+ SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParser.ReadNetworkPacketAsyncCallback | INFO | ADV | State Object Id {0}, Packet read. In Buffer: {1}, In Bytes Read: {2}", ObjectID, _inBuff, _inBytesRead);
+
+ AssertValidState();
+ }
+ else
+ {
+ throw SQL.ParsingError(ParsingErrorState.ProcessSniPacketFailed);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs
index b3cf628594..fe5ce76614 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs
@@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Security;
+using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -64,6 +65,13 @@ public TimeoutState(int value)
public int IdentityValue => _value;
}
+ private enum SnapshotStatus
+ {
+ NotActive,
+ ReplayStarting,
+ ReplayRunning
+ }
+
private const int AttentionTimeoutSeconds = 5;
// Ticks to consider a connection "good" after a successful I/O (10,000 ticks = 1 ms)
@@ -215,7 +223,7 @@ public TimeoutState(int value)
internal TaskCompletionSource