diff --git a/src/Renci.SshNet/Sftp/SftpSession.cs b/src/Renci.SshNet/Sftp/SftpSession.cs index 1bbb89012..2d651c318 100644 --- a/src/Renci.SshNet/Sftp/SftpSession.cs +++ b/src/Renci.SshNet/Sftp/SftpSession.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -22,8 +23,8 @@ internal sealed class SftpSession : SubsystemSession, ISftpSession private readonly Dictionary _requests = new Dictionary(); private readonly ISftpResponseFactory _sftpResponseFactory; - private readonly List _data = new List(32 * 1024); private readonly Encoding _encoding; + private System.Net.ArrayBuffer _buffer = new(32 * 1024); private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(initialState: false); private IDictionary _supportedExtensions; @@ -303,125 +304,77 @@ protected override void OnChannelOpen() protected override void OnDataReceived(byte[] data) { - const int packetLengthByteCount = 4; - const int sftpMessageTypeByteCount = 1; - const int minimumChannelDataLength = packetLengthByteCount + sftpMessageTypeByteCount; + ArraySegment d = new(data); - var offset = 0; - var count = data.Length; - - // improve performance and reduce GC pressure by not buffering channel data if the received - // chunk contains the complete packet data. - // - // for this, the buffer should be empty and the chunk should contain at least the packet length - // and the type of the SFTP message - if (_data.Count == 0) + // If the buffer is empty then skip a copy and read packets + // directly out of the given data. + if (_buffer.ActiveLength == 0) { - while (count >= minimumChannelDataLength) + while (d.Count >= 4) { - // extract packet length - var packetDataLength = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | - data[offset + 3]; - - var packetTotalLength = packetDataLength + packetLengthByteCount; + var packetLength = BinaryPrimitives.ReadInt32BigEndian(d); - // check if complete packet data (or more) is available - if (count >= packetTotalLength) + if (d.Count - 4 < packetLength) { - // load and process SFTP message - if (!TryLoadSftpMessage(data, offset + packetLengthByteCount, packetDataLength)) - { - return; - } - - // remove processed bytes from the number of bytes to process as the channel - // data we received may contain (part of) another message - count -= packetTotalLength; - - // move offset beyond bytes we just processed - offset += packetTotalLength; + break; } - else + + if (!TryLoadSftpMessage(d.Slice(4, packetLength))) { - // we don't have a complete message - break; + // An error occured. + return; } - } - // check if there is channel data left to process or buffer - if (count == 0) - { - return; + d = d.Slice(4 + packetLength); } - // check if we processed part of the channel data we received - if (offset > 0) - { - // add (remaining) channel data to internal data holder - var remainingChannelData = new byte[count]; - Buffer.BlockCopy(data, offset, remainingChannelData, 0, count); - _data.AddRange(remainingChannelData); - } - else + if (d.Count > 0) { - // add (remaining) channel data to internal data holder - _data.AddRange(data); + // Now buffer the remainder. + _buffer.EnsureAvailableSpace(d.Count); + d.AsSpan().CopyTo(_buffer.AvailableSpan); + _buffer.Commit(d.Count); } - // skip further processing as we'll need a new chunk to complete the message return; } - // add (remaining) channel data to internal data holder - _data.AddRange(data); + // The buffer already had some data. Append the new data and + // proceed with reading out packets. + _buffer.EnsureAvailableSpace(d.Count); + d.AsSpan().CopyTo(_buffer.AvailableSpan); + _buffer.Commit(d.Count); - while (_data.Count >= minimumChannelDataLength) + while (_buffer.ActiveLength >= 4) { - // extract packet length - var packetDataLength = _data[0] << 24 | _data[1] << 16 | _data[2] << 8 | _data[3]; + d = new ArraySegment( + _buffer.DangerousGetUnderlyingBuffer(), + _buffer.ActiveStartOffset, + _buffer.ActiveLength); - var packetTotalLength = packetDataLength + packetLengthByteCount; + var packetLength = BinaryPrimitives.ReadInt32BigEndian(d); - // check if complete packet data is available - if (_data.Count < packetTotalLength) + if (d.Count - 4 < packetLength) { - // wait for complete message to arrive first break; } - // create buffer to hold packet data - var packetData = new byte[packetDataLength]; + // Note: the packet data in the buffer is safe to read from + // only for the duration of this load. If it needs to be stored, + // callees should make their own copy. + _ = TryLoadSftpMessage(d.Slice(4, packetLength)); - // copy packet data and bytes for length to array - _data.CopyTo(packetLengthByteCount, packetData, 0, packetDataLength); - - // remove loaded data and bytes for length from _data holder - if (_data.Count == packetTotalLength) - { - // the only buffered data is the data we're processing - _data.Clear(); - } - else - { - // remove only the data we're processing - _data.RemoveRange(0, packetTotalLength); - } - - // load and process SFTP message - if (!TryLoadSftpMessage(packetData, 0, packetDataLength)) - { - break; - } + _buffer.Discard(4 + packetLength); } } - private bool TryLoadSftpMessage(byte[] packetData, int offset, int count) + private bool TryLoadSftpMessage(ArraySegment packetData) { // Create SFTP message - var response = _sftpResponseFactory.Create(ProtocolVersion, packetData[offset], _encoding); + var response = _sftpResponseFactory.Create(ProtocolVersion, packetData.Array[packetData.Offset], _encoding); // Load message data into it - response.Load(packetData, offset + 1, count - 1); + response.Load(packetData.Array, packetData.Offset + 1, packetData.Count - 1); try {