Browse Source

Build the read-ahead mechanism into SftpFileStream (#1705)

* Build the read-ahead mechanism into SftpFileStream

This change unifies the SFTP download implementations that exist via DownloadFile and
via SftpFileStream, by rewriting SftpFileStream to perform the same "read-aheads" as
DownloadFile. This brings the performance of downloads via SftpFileStream in line with
DownloadFile, such that the latter is now effectively SftpFileStream.CopyTo. It also
brings the recently added DownloadFileAsync up to speed since that was implemented via
SftpFileStream.CopyToAsync.

The methodology is a mix of the previous one and that within OpenSSH: the first call to
SftpFileStream.Read sends one read request to the server. The second sends two and when
not interrupted by Write or similar, the number of in-flight read requests continues to
scale up in this fashion.

I have measured CopyTo to be 3-20x faster than before, depending on file size and server
round-trip time.

* Check CanSeek in ReadAllBytes

* Squeeze out some performance
Rob Hague 3 days ago
parent
commit
dccdedc36e
38 changed files with 979 additions and 4717 deletions
  1. 24 0
      src/Renci.SshNet/Common/Extensions.cs
  2. 0 12
      src/Renci.SshNet/IServiceFactory.cs
  3. 0 47
      src/Renci.SshNet/ServiceFactory.cs
  4. 0 23
      src/Renci.SshNet/Sftp/ISftpFileReader.cs
  5. 1 16
      src/Renci.SshNet/Sftp/ISftpSession.cs
  6. 146 410
      src/Renci.SshNet/Sftp/SftpFileReader.cs
  7. 435 825
      src/Renci.SshNet/Sftp/SftpFileStream.cs
  8. 5 27
      src/Renci.SshNet/Sftp/SftpSession.cs
  9. 122 47
      src/Renci.SshNet/SftpClient.cs
  10. 1 1
      test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs
  11. 196 53
      test/Renci.SshNet.IntegrationTests/SftpTests.cs
  12. 0 99
      test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs
  13. 0 101
      test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs
  14. 0 101
      test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs
  15. 0 101
      test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs
  16. 0 101
      test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs
  17. 0 101
      test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs
  18. 0 103
      test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanMaxPendingReadsTimesChunkSize.cs
  19. 0 101
      test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs
  20. 0 63
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTestBase.cs
  21. 0 173
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead.cs
  22. 0 132
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsNotOpen.cs
  23. 0 136
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsOpen_BeginCloseThrowsException.cs
  24. 0 141
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsOpen_EndCloseThrowsException.cs
  25. 0 168
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsComplete.cs
  26. 0 167
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsPartial.cs
  27. 0 323
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsNotReached.cs
  28. 0 207
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsReached.cs
  29. 0 6
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadBeginReadException.cs
  30. 0 213
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadEndInvokeException_DiscardsFurtherReadAheads.cs
  31. 0 189
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadEndInvokeException_PreventsFurtherReadAheads.cs
  32. 0 6
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadBackBeginReadException.cs
  33. 0 6
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadBackEndInvokeException.cs
  34. 0 172
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInBeginRead.cs
  35. 0 144
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_ChunkAvailable.cs
  36. 0 128
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_NoChunkAvailable.cs
  37. 0 70
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamAsyncTestBase.cs
  38. 49 4
      test/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest.cs

+ 24 - 0
src/Renci.SshNet/Common/Extensions.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 #if !NET
 using System.IO;
+using System.Threading.Tasks;
 #endif
 using System.Net;
 using System.Net.Sockets;
@@ -398,6 +399,29 @@ namespace Renci.SshNet.Common
                 totalRead += read;
             }
         }
+
+        internal static Task<T> WaitAsync<T>(this Task<T> task, CancellationToken cancellationToken)
+        {
+            if (task.IsCompleted || !cancellationToken.CanBeCanceled)
+            {
+                return task;
+            }
+
+            return WaitCore();
+
+            async Task<T> WaitCore()
+            {
+                TaskCompletionSource<T> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+                using var reg = cancellationToken.Register(
+                    () => tcs.TrySetCanceled(cancellationToken),
+                    useSynchronizationContext: false);
+
+                var completedTask = await Task.WhenAny(task, tcs.Task).ConfigureAwait(false);
+
+                return await completedTask.ConfigureAwait(false);
+            }
+        }
 #endif
     }
 }

+ 0 - 12
src/Renci.SshNet/IServiceFactory.cs

@@ -83,18 +83,6 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">No key exchange algorithm is supported by both client and server.</exception>
         IKeyExchange CreateKeyExchange(IDictionary<string, Func<IKeyExchange>> clientAlgorithms, string[] serverAlgorithms);
 
-        /// <summary>
-        /// Creates an <see cref="ISftpFileReader"/> for the specified file and with the specified
-        /// buffer size.
-        /// </summary>
-        /// <param name="fileName">The file to read.</param>
-        /// <param name="sftpSession">The SFTP session to use.</param>
-        /// <param name="bufferSize">The size of buffer.</param>
-        /// <returns>
-        /// An <see cref="ISftpFileReader"/>.
-        /// </returns>
-        ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize);
-
         /// <summary>
         /// Creates a new <see cref="ISftpResponseFactory"/> instance.
         /// </summary>

+ 0 - 47
src/Renci.SshNet/ServiceFactory.cs

@@ -4,8 +4,6 @@ using System.Linq;
 using System.Net.Sockets;
 using System.Text;
 
-using Microsoft.Extensions.Logging;
-
 using Renci.SshNet.Common;
 using Renci.SshNet.Connection;
 using Renci.SshNet.Messages.Transport;
@@ -118,51 +116,6 @@ namespace Renci.SshNet
             return new NetConfSession(session, operationTimeout);
         }
 
-        /// <summary>
-        /// Creates an <see cref="ISftpFileReader"/> for the specified file and with the specified
-        /// buffer size.
-        /// </summary>
-        /// <param name="fileName">The file to read.</param>
-        /// <param name="sftpSession">The SFTP session to use.</param>
-        /// <param name="bufferSize">The size of buffer.</param>
-        /// <returns>
-        /// An <see cref="ISftpFileReader"/>.
-        /// </returns>
-        public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize)
-        {
-            const int defaultMaxPendingReads = 10;
-
-            // Issue #292: Avoid overlapping SSH_FXP_OPEN and SSH_FXP_LSTAT requests for the same file as this
-            // causes a performance degradation on Sun SSH
-            var openAsyncResult = sftpSession.BeginOpen(fileName, Flags.Read, callback: null, state: null);
-            var handle = sftpSession.EndOpen(openAsyncResult);
-
-            var statAsyncResult = sftpSession.BeginLStat(fileName, callback: null, state: null);
-
-            long? fileSize;
-            int maxPendingReads;
-
-            var chunkSize = sftpSession.CalculateOptimalReadLength(bufferSize);
-
-            // fallback to a default maximum of pending reads when remote server does not allow us to obtain
-            // the attributes of the file
-            try
-            {
-                var fileAttributes = sftpSession.EndLStat(statAsyncResult);
-                fileSize = fileAttributes.Size;
-                maxPendingReads = Math.Min(100, (int)Math.Ceiling((double)fileAttributes.Size / chunkSize) + 1);
-            }
-            catch (SshException ex)
-            {
-                fileSize = null;
-                maxPendingReads = defaultMaxPendingReads;
-
-                sftpSession.SessionLoggerFactory.CreateLogger<ServiceFactory>().LogInformation(ex, "Failed to obtain size of file. Allowing maximum {MaxPendingReads} pending reads", maxPendingReads);
-            }
-
-            return sftpSession.CreateFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
-        }
-
         /// <summary>
         /// Creates a new <see cref="ISftpResponseFactory"/> instance.
         /// </summary>

+ 0 - 23
src/Renci.SshNet/Sftp/ISftpFileReader.cs

@@ -1,23 +0,0 @@
-using System;
-
-using Renci.SshNet.Common;
-
-namespace Renci.SshNet.Sftp
-{
-    /// <summary>
-    /// Reads a given file.
-    /// </summary>
-    internal interface ISftpFileReader : IDisposable
-    {
-        /// <summary>
-        /// Reads a sequence of bytes from the current file and advances the position within the file by the number of bytes read.
-        /// </summary>
-        /// <returns>
-        /// The sequence of bytes read from the file, or a zero-length array if the end of the file
-        /// has been reached.
-        /// </returns>
-        /// <exception cref="ObjectDisposedException">The current <see cref="ISftpFileReader"/> is disposed.</exception>
-        /// <exception cref="SshException">Attempting to read beyond the end of the file.</exception>
-        byte[] Read();
-    }
-}

+ 1 - 16
src/Renci.SshNet/Sftp/ISftpSession.cs

@@ -67,11 +67,10 @@ namespace Renci.SshNet.Sftp
         /// Asynchronously performs a <c>SSH_FXP_FSTAT</c> request.
         /// </summary>
         /// <param name="handle">The handle.</param>
-        /// <param name="nullOnError">If set to <see langword="true"/>, <see langword="null"/> is returned in case of an error.</param>
         /// <returns>
         /// The file attributes.
         /// </returns>
-        SftpFileAttributes RequestFStat(byte[] handle, bool nullOnError);
+        SftpFileAttributes RequestFStat(byte[] handle);
 
         /// <summary>
         /// Asynchronously performs a <c>SSH_FXP_FSTAT</c> request.
@@ -522,19 +521,5 @@ namespace Renci.SshNet.Sftp
         /// Currently, we do not take the remote window size into account.
         /// </remarks>
         uint CalculateOptimalWriteLength(uint bufferSize, byte[] handle);
-
-        /// <summary>
-        /// Creates an <see cref="ISftpFileReader"/> for reading the content of the file represented by a given <paramref name="handle"/>.
-        /// </summary>
-        /// <param name="handle">The handle of the file to read.</param>
-        /// <param name="sftpSession">The SFTP session.</param>
-        /// <param name="chunkSize">The maximum number of bytes to read with each chunk.</param>
-        /// <param name="maxPendingReads">The maximum number of pending reads.</param>
-        /// <param name="fileSize">The size of the file or <see langword="null"/> when the size could not be determined.</param>
-        /// <returns>
-        /// An <see cref="ISftpFileReader"/> for reading the content of the file represented by the
-        /// specified <paramref name="handle"/>.
-        /// </returns>
-        ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize);
     }
 }

+ 146 - 410
src/Renci.SshNet/Sftp/SftpFileReader.cs

@@ -1,469 +1,205 @@
-using System;
+#nullable enable
+using System;
 using System.Collections.Generic;
-using System.Globalization;
+using System.Diagnostics;
 using System.Runtime.ExceptionServices;
 using System.Threading;
+using System.Threading.Tasks;
 
-using Microsoft.Extensions.Logging;
-
-using Renci.SshNet.Abstractions;
+#if !NET
 using Renci.SshNet.Common;
+#endif
 
 namespace Renci.SshNet.Sftp
 {
-    internal sealed class SftpFileReader : ISftpFileReader
+    public sealed partial class SftpFileStream
     {
-        private const int ReadAheadWaitTimeoutInMilliseconds = 1000;
-
-        private readonly byte[] _handle;
-        private readonly ISftpSession _sftpSession;
-        private readonly uint _chunkSize;
-        private readonly SemaphoreSlim _semaphore;
-        private readonly object _readLock;
-        private readonly ManualResetEvent _disposingWaitHandle;
-        private readonly ManualResetEvent _readAheadCompleted;
-        private readonly Dictionary<int, BufferedRead> _queue;
-        private readonly WaitHandle[] _waitHandles;
-        private readonly ILogger _logger;
-
-        /// <summary>
-        /// Holds the size of the file, when available.
-        /// </summary>
-        private readonly long? _fileSize;
-
-        private ulong _offset;
-        private int _readAheadChunkIndex;
-        private ulong _readAheadOffset;
-        private int _nextChunkIndex;
-
-        /// <summary>
-        /// Holds a value indicating whether EOF has already been signaled by the SSH server.
-        /// </summary>
-        private bool _endOfFileReceived;
-
-        /// <summary>
-        /// Holds a value indicating whether the client has read up to the end of the file.
-        /// </summary>
-        private bool _isEndOfFileRead;
-
-        private bool _disposingOrDisposed;
-
-        private Exception _exception;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SftpFileReader"/> class with the specified handle,
-        /// <see cref="ISftpSession"/> and the maximum number of pending reads.
-        /// </summary>
-        /// <param name="handle">The file handle.</param>
-        /// <param name="sftpSession">The SFT session.</param>
-        /// <param name="chunkSize">The size of a individual read-ahead chunk.</param>
-        /// <param name="maxPendingReads">The maximum number of pending reads.</param>
-        /// <param name="fileSize">The size of the file, if known; otherwise, <see langword="null"/>.</param>
-        public SftpFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize)
-        {
-            _handle = handle;
-            _sftpSession = sftpSession;
-            _chunkSize = chunkSize;
-            _fileSize = fileSize;
-            _semaphore = new SemaphoreSlim(maxPendingReads);
-            _queue = new Dictionary<int, BufferedRead>(maxPendingReads);
-            _readLock = new object();
-            _readAheadCompleted = new ManualResetEvent(initialState: false);
-            _disposingWaitHandle = new ManualResetEvent(initialState: false);
-            _waitHandles = _sftpSession.CreateWaitHandleArray(_disposingWaitHandle, _semaphore.AvailableWaitHandle);
-            _logger = sftpSession.SessionLoggerFactory.CreateLogger<SftpFileReader>();
-
-            StartReadAhead();
-        }
-
-        public byte[] Read()
+        private sealed class SftpFileReader : IDisposable
         {
-            ThrowHelper.ThrowObjectDisposedIf(_disposingOrDisposed, this);
-
-            if (_exception is not null)
-            {
-                ExceptionDispatchInfo.Capture(_exception).Throw();
-            }
-
-            if (_isEndOfFileRead)
-            {
-                throw new SshException("Attempting to read beyond the end of the file.");
-            }
-
-            BufferedRead nextChunk;
-
-            lock (_readLock)
-            {
-                // wait until either the next chunk is available, an exception has occurred or the current
-                // instance is already disposed
-                while (!_queue.TryGetValue(_nextChunkIndex, out nextChunk) && _exception is null)
-                {
-                    _ = Monitor.Wait(_readLock);
-                }
-
-                // throw when exception occured in read-ahead, or the current instance is already disposed
-                if (_exception != null)
+            private readonly byte[] _handle;
+            private readonly ISftpSession _sftpSession;
+            private readonly int _maxPendingReads;
+            private readonly ulong? _fileSize;
+            private readonly Dictionary<ulong, Request> _requests = [];
+            private readonly CancellationTokenSource _cts;
+
+            private uint _chunkSize;
+
+            private ulong _offset;
+            private ulong _readAheadOffset;
+            private int _currentMaxRequests;
+            private ExceptionDispatchInfo? _exception;
+
+            /// <summary>
+            /// Initializes a new instance of the <see cref="SftpFileReader"/> class with the specified handle,
+            /// <see cref="ISftpSession"/> and the maximum number of pending reads.
+            /// </summary>
+            /// <param name="handle">The file handle.</param>
+            /// <param name="sftpSession">The SFTP session.</param>
+            /// <param name="chunkSize">The size of a individual read-ahead chunk.</param>
+            /// <param name="position">The starting offset in the file.</param>
+            /// <param name="maxPendingReads">The maximum number of pending reads.</param>
+            /// <param name="fileSize">The size of the file, if known; otherwise, <see langword="null"/>.</param>
+            /// <param name="initialMaxRequests">The initial number of pending reads.</param>
+            public SftpFileReader(byte[] handle, ISftpSession sftpSession, int chunkSize, long position, int maxPendingReads, ulong? fileSize = null, int initialMaxRequests = 1)
+            {
+                Debug.Assert(chunkSize > 0);
+                Debug.Assert(position >= 0);
+                Debug.Assert(initialMaxRequests >= 1);
+
+                _handle = handle;
+                _sftpSession = sftpSession;
+                _chunkSize = (uint)chunkSize;
+                _offset = _readAheadOffset = (ulong)position;
+                _maxPendingReads = maxPendingReads;
+                _fileSize = fileSize;
+                _currentMaxRequests = initialMaxRequests;
+
+                _cts = new CancellationTokenSource();
+            }
+
+            public async Task<byte[]> ReadAsync(CancellationToken cancellationToken)
+            {
+                _exception?.Throw();
+
+                try
                 {
-                    ExceptionDispatchInfo.Capture(_exception).Throw();
-                }
-
-                var data = nextChunk.Data;
-
-                if (nextChunk.Offset == _offset)
-                {
-                    // have we reached EOF?
-                    if (data.Length == 0)
-                    {
-                        // PERF: we do not bother updating all of the internal state when we've reached EOF
-                        _isEndOfFileRead = true;
-                    }
-                    else
+                    // Fill up the requests buffer with as many requests as we currently allow.
+                    // On the first call to Read, that number is 1. On the second it is 2 etc.
+                    while (_requests.Count < _currentMaxRequests)
                     {
-                        // remove processed chunk
-                        _ = _queue.Remove(_nextChunkIndex);
-
-                        // update offset
-                        _offset += (ulong)data.Length;
+                        AddRequest(_readAheadOffset, _chunkSize);
 
-                        // move to next chunk
-                        _nextChunkIndex++;
+                        _readAheadOffset += _chunkSize;
                     }
 
-                    // unblock wait in read-ahead
-                    _ = _semaphore.Release();
+                    var request = _requests[_offset];
 
-                    return data;
-                }
-
-                // When we received an EOF for the next chunk and the size of the file is known, then
-                // we only complete the current chunk if we haven't already read up to the file size.
-                // This way we save an extra round-trip to the server.
-                if (data.Length == 0 && _fileSize.HasValue && _offset == (ulong)_fileSize.Value)
-                {
-                    // avoid future reads
-                    _isEndOfFileRead = true;
-
-                    // unblock wait in read-ahead
-                    _ = _semaphore.Release();
-
-                    // signal EOF to caller
-                    return nextChunk.Data;
-                }
-            }
+                    var data = await request.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
 
-            /*
-             * When the server returned less bytes than requested (for the previous chunk)
-             * we'll synchronously request the remaining data.
-             *
-             * Due to the optimization above, we'll only get here in one of the following cases:
-             * - an EOF situation for files for which we were unable to obtain the file size
-             * - fewer bytes that requested were returned
-             *
-             * According to the SSH specification, this last case should never happen for normal
-             * disk files (but can happen for device files). In practice, OpenSSH - for example -
-             * returns less bytes than requested when requesting more than 64 KB.
-             *
-             * Important:
-             * To avoid a deadlock, this read must be done outside of the read lock.
-             */
-
-            var bytesToCatchUp = nextChunk.Offset - _offset;
-
-            /*
-             * TODO: break loop and interrupt blocking wait in case of exception
-             */
-
-            var read = _sftpSession.RequestRead(_handle, _offset, (uint)bytesToCatchUp);
-            if (read.Length == 0)
-            {
-                // process data in read lock to avoid ObjectDisposedException while releasing semaphore
-                lock (_readLock)
-                {
-                    // a zero-length (EOF) response is only valid for the read-back when EOF has
-                    // been signaled for the next read-ahead chunk
-                    if (nextChunk.Data.Length == 0)
-                    {
-                        _isEndOfFileRead = true;
-
-                        // ensure we've not yet disposed the current instance
-                        if (!_disposingOrDisposed)
-                        {
-                            // unblock wait in read-ahead
-                            _ = _semaphore.Release();
-                        }
-
-                        // signal EOF to caller
-                        return read;
-                    }
-
-                    // move reader to error state
-                    _exception = new SshException("Unexpectedly reached end of file.");
-
-                    // ensure we've not yet disposed the current instance
-                    if (!_disposingOrDisposed)
+                    if (data.Length == 0)
                     {
-                        // unblock wait in read-ahead
-                        _ = _semaphore.Release();
+                        // EOF. We effectively disable this instance - further reads will
+                        // continue to return EOF.
+                        _currentMaxRequests = 0;
+                        return data;
                     }
 
-                    // notify caller of error
-                    throw _exception;
-                }
-            }
-
-            _offset += (uint)read.Length;
+                    _ = _requests.Remove(_offset);
 
-            return read;
-        }
+                    _offset += (ulong)data.Length;
 
-        public void Dispose()
-        {
-            Dispose(disposing: true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
-        private void Dispose(bool disposing)
-        {
-            if (_disposingOrDisposed)
-            {
-                return;
-            }
-
-            // transition to disposing state
-            _disposingOrDisposed = true;
-
-            if (disposing)
-            {
-                // record exception to break prevent future Read()
-                _exception = new ObjectDisposedException(GetType().FullName);
-
-                // signal that we're disposing to interrupt wait in read-ahead
-                _ = _disposingWaitHandle.Set();
-
-                // wait until the read-ahead thread has completed
-                _ = _readAheadCompleted.WaitOne();
-
-                // unblock the Read()
-                lock (_readLock)
-                {
-                    // dispose semaphore in read lock to ensure we don't run into an ObjectDisposedException
-                    // in Read()
-                    _semaphore.Dispose();
-
-                    // awake Read
-                    Monitor.PulseAll(_readLock);
-                }
+                    if (data.Length < request.Count)
+                    {
+                        // We didn't receive all the data we requested.
 
-                _readAheadCompleted.Dispose();
-                _disposingWaitHandle.Dispose();
+                        // If we've read exactly up to our known file size and the next
+                        // request is already in-flight, then wait for it and if it signals
+                        // EOF (as is likely), then call EOF here and omit a final round-trip.
+                        // This optimisation is mostly only beneficial to smaller files on
+                        // higher latency connections.
 
-                if (_sftpSession.IsOpen)
-                {
-                    try
-                    {
-                        var closeAsyncResult = _sftpSession.BeginClose(_handle, callback: null, state: null);
-                        _sftpSession.EndClose(closeAsyncResult);
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogInformation(ex, "Failure closing handle");
-                    }
-                }
-            }
-        }
+                        var nextRequestOffset = _offset - (ulong)data.Length + request.Count;
 
-        private void StartReadAhead()
-        {
-            ThreadAbstraction.ExecuteThread(() =>
-            {
-                while (!_endOfFileReceived && _exception is null)
-                {
-                    // check if we should continue with the read-ahead loop
-                    // note that the EOF and exception check are not included
-                    // in this check as they do not require Read() to be
-                    // unblocked (or have already done this)
-                    if (!ContinueReadAhead())
-                    {
-                        // unblock the Read()
-                        lock (_readLock)
+                        if (_offset == _fileSize &&
+                            _requests.TryGetValue(nextRequestOffset, out var nextRequest))
                         {
-                            Monitor.PulseAll(_readLock);
+                            var nextRequestData = await nextRequest.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+                            if (nextRequestData.Length == 0)
+                            {
+                                _offset = nextRequestOffset;
+                                _currentMaxRequests = 0;
+                                return data;
+                            }
                         }
 
-                        // break the read-ahead loop
-                        break;
-                    }
-
-                    // attempt to obtain the semaphore; this may time out when all semaphores are
-                    // in use due to pending read-aheads (which in turn can happen when the server
-                    // is slow to respond or when the session is broken)
-                    if (!_semaphore.Wait(ReadAheadWaitTimeoutInMilliseconds))
-                    {
-                        // re-evaluate whether an exception occurred, and - if not - wait again
-                        continue;
-                    }
+                        // Otherwise, add another request to fill in the gap.
+                        AddRequest(_offset, request.Count - (uint)data.Length);
 
-                    // don't bother reading any more chunks if we received EOF, an exception has occurred
-                    // or the current instance is disposed
-                    if (_endOfFileReceived || _exception != null)
-                    {
-                        break;
+                        if (data.Length < _chunkSize)
+                        {
+                            // Right-size the buffer to match the amount that the server
+                            // is willing to return.
+                            // Note that this also happens near EOF.
+                            _chunkSize = Math.Max(512, (uint)data.Length);
+                        }
                     }
 
-                    // start reading next chunk
-                    var bufferedRead = new BufferedRead(_readAheadChunkIndex, _readAheadOffset);
-
-                    try
+                    if (_currentMaxRequests > 0)
                     {
-                        // even if we know the size of the file and have read up to EOF, we still want
-                        // to keep reading (ahead) until we receive zero bytes from the remote host as
-                        // we do not want to rely purely on the reported file size
-                        //
-                        // if the offset of the read-ahead chunk is greater than that file size, then
-                        // we can expect to be reading the last (zero-byte) chunk and switch to synchronous
-                        // mode to avoid having multiple read-aheads that read beyond EOF
-                        if (_fileSize != null && (long)_readAheadOffset > _fileSize.Value)
+                        if (_readAheadOffset > _fileSize + _chunkSize)
                         {
-                            var asyncResult = _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkSize, callback: null, bufferedRead);
-                            var data = _sftpSession.EndRead(asyncResult);
-                            ReadCompletedCore(bufferedRead, data);
+                            // If the file size is known and we've got requests
+                            // out beyond that (plus a buffer for EOD read), then
+                            // restrict the number of outgoing requests.
+                            // This does nothing for the performance of this download
+                            // but may reduce traffic for other downloads.
+                            _currentMaxRequests = 1;
                         }
-                        else
+                        else if (_currentMaxRequests < _maxPendingReads)
                         {
-                            _ = _sftpSession.BeginRead(_handle, _readAheadOffset, _chunkSize, ReadCompleted, bufferedRead);
+                            _currentMaxRequests++;
                         }
                     }
-                    catch (Exception ex)
-                    {
-                        HandleFailure(ex);
-                        break;
-                    }
 
-                    // advance read-ahead offset
-                    _readAheadOffset += _chunkSize;
-
-                    // increment index of read-ahead chunk
-                    _readAheadChunkIndex++;
+                    return data;
                 }
-
-                _ = _readAheadCompleted.Set();
-            });
-        }
-
-        /// <summary>
-        /// Returns a value indicating whether the read-ahead loop should be continued.
-        /// </summary>
-        /// <returns>
-        /// <see langword="true"/> if the read-ahead loop should be continued; otherwise, <see langword="false"/>.
-        /// </returns>
-        private bool ContinueReadAhead()
-        {
-            try
-            {
-                var waitResult = _sftpSession.WaitAny(_waitHandles, _sftpSession.OperationTimeout);
-                switch (waitResult)
+                catch (Exception ex) when (!(ex is OperationCanceledException oce && oce.CancellationToken == cancellationToken))
                 {
-                    case 0: // disposing
-                        return false;
-                    case 1: // semaphore available
-                        return true;
-                    default:
-                        throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "WaitAny return value '{0}' is not implemented.", waitResult));
+                    // If the wait was cancelled then we will allow subsequent reads as normal.
+                    // For any other errors, we prevent further read requests, effectively disabling
+                    // this instance.
+                    _currentMaxRequests = 0;
+                    _exception = ExceptionDispatchInfo.Capture(ex);
+                    throw;
                 }
             }
-            catch (Exception ex)
-            {
-                _ = Interlocked.CompareExchange(ref _exception, ex, comparand: null);
-                return false;
-            }
-        }
 
-        private void ReadCompleted(IAsyncResult result)
-        {
-            if (_disposingOrDisposed)
+            private void AddRequest(ulong offset, uint count)
             {
-                // skip further processing if we're disposing the current instance
-                // to avoid accessing disposed members
-                return;
+                _requests.Add(
+                    offset,
+                    new Request(
+                        offset,
+                        count,
+                        _sftpSession.RequestReadAsync(_handle, offset, count, _cts.Token)));
             }
 
-            var readAsyncResult = (SftpReadAsyncResult)result;
-
-            byte[] data;
-
-            try
-            {
-                data = readAsyncResult.EndInvoke();
-            }
-            catch (Exception ex)
+            public void Dispose()
             {
-                HandleFailure(ex);
-                return;
-            }
-
-            // a read that completes with a zero-byte result signals EOF
-            // but there may be pending reads before that read
-            var bufferedRead = (BufferedRead)readAsyncResult.AsyncState;
-            ReadCompletedCore(bufferedRead, data);
-        }
+                _exception ??= ExceptionDispatchInfo.Capture(new ObjectDisposedException(GetType().FullName));
 
-        private void ReadCompletedCore(BufferedRead bufferedRead, byte[] data)
-        {
-            bufferedRead.Complete(data);
-
-            lock (_readLock)
-            {
-                // add item to queue
-                _queue.Add(bufferedRead.ChunkIndex, bufferedRead);
+                if (_requests.Count > 0)
+                {
+                    // Cancel outstanding requests and observe the exception on them
+                    // as an effort to prevent unhandled exceptions.
 
-                // Signal that a chunk has been read or EOF has been reached.
-                // In both cases, Read() will eventually also unblock the "read-ahead" thread.
-                Monitor.PulseAll(_readLock);
-            }
+                    _cts.Cancel();
 
-            // check if server signaled EOF
-            if (data.Length == 0)
-            {
-                // set a flag to stop read-aheads
-                _endOfFileReceived = true;
-            }
-        }
-
-        private void HandleFailure(Exception cause)
-        {
-            _ = Interlocked.CompareExchange(ref _exception, cause, comparand: null);
+                    foreach (var request in _requests.Values)
+                    {
+                        _ = request.Task.Exception;
+                    }
 
-            // unblock read-ahead
-            _ = _semaphore.Release();
+                    _requests.Clear();
+                }
 
-            // unblock Read()
-            lock (_readLock)
-            {
-                Monitor.PulseAll(_readLock);
+                _cts.Dispose();
             }
-        }
 
-        internal sealed class BufferedRead
-        {
-            public int ChunkIndex { get; }
-
-            public byte[] Data { get; private set; }
-
-            public ulong Offset { get; }
-
-            public BufferedRead(int chunkIndex, ulong offset)
+            private sealed class Request
             {
-                ChunkIndex = chunkIndex;
-                Offset = offset;
-            }
+                public Request(ulong offset, uint count, Task<byte[]> task)
+                {
+                    Offset = offset;
+                    Count = count;
+                    Task = task;
+                }
 
-            public void Complete(byte[] data)
-            {
-                Data = data;
+                public ulong Offset { get; }
+                public uint Count { get; }
+                public Task<byte[]> Task { get; }
             }
         }
     }

+ 435 - 825
src/Renci.SshNet/Sftp/SftpFileStream.cs

@@ -1,145 +1,90 @@
-using System;
+#nullable enable
+using System;
+using System.ComponentModel;
 using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
 
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Common;
 
 namespace Renci.SshNet.Sftp
 {
     /// <summary>
-    /// Exposes a <see cref="Stream"/> around a remote SFTP file, supporting both synchronous and asynchronous read and write operations.
+    /// Exposes a <see cref="Stream"/> around a remote SFTP file, supporting
+    /// both synchronous and asynchronous read and write operations.
     /// </summary>
-    /// <threadsafety static="true" instance="false"/>
-#pragma warning disable IDE0079 // We intentionally want to suppress the below warning.
-    [SuppressMessage("Performance", "CA1844: Provide memory-based overrides of async methods when subclassing 'Stream'", Justification = "TODO: This should be addressed in the future.")]
-#pragma warning restore IDE0079
-    public sealed class SftpFileStream : Stream
+    public sealed partial class SftpFileStream : Stream
     {
-        private readonly Lock _lock = new Lock();
+        private const int MaxPendingReads = 100;
+
+        private readonly ISftpSession _session;
+        private readonly FileAccess _access;
+        private readonly bool _canSeek;
         private readonly int _readBufferSize;
-        private readonly int _writeBufferSize;
 
-        // Internal state.
-        private byte[] _handle;
-        private ISftpSession _session;
+        private SftpFileReader? _sftpFileReader;
+        private ReadOnlyMemory<byte> _readBuffer;
+        private System.Net.ArrayBuffer _writeBuffer;
 
-        // Buffer information.
-        private byte[] _readBuffer;
-        private byte[] _writeBuffer;
-        private int _bufferPosition;
-        private int _bufferLen;
         private long _position;
-        private bool _bufferOwnedByWrite;
-        private bool _canRead;
-        private bool _canSeek;
-        private bool _canWrite;
         private TimeSpan _timeout;
+        private bool _disposed;
 
-        /// <summary>
-        /// Gets a value indicating whether the current stream supports reading.
-        /// </summary>
-        /// <value>
-        /// <see langword="true"/> if the stream supports reading; otherwise, <see langword="false"/>.
-        /// </value>
+        /// <inheritdoc/>
         public override bool CanRead
         {
-            get { return _canRead; }
+            get { return !_disposed && (_access & FileAccess.Read) == FileAccess.Read; }
         }
 
-        /// <summary>
-        /// Gets a value indicating whether the current stream supports seeking.
-        /// </summary>
-        /// <value>
-        /// <see langword="true"/> if the stream supports seeking; otherwise, <see langword="false"/>.
-        /// </value>
+        /// <inheritdoc/>
         public override bool CanSeek
         {
-            get { return _canSeek; }
+            get { return !_disposed && _canSeek; }
         }
 
-        /// <summary>
-        /// Gets a value indicating whether the current stream supports writing.
-        /// </summary>
-        /// <value>
-        /// <see langword="true"/> if the stream supports writing; otherwise, <see langword="false"/>.
-        /// </value>
+        /// <inheritdoc/>
         public override bool CanWrite
         {
-            get { return _canWrite; }
+            get { return !_disposed && (_access & FileAccess.Write) == FileAccess.Write; }
         }
 
         /// <summary>
         /// Gets a value indicating whether timeout properties are usable for <see cref="SftpFileStream"/>.
         /// </summary>
         /// <value>
-        /// <see langword="true"/> in all cases.
+        /// <see langword="false"/> in all cases.
         /// </value>
         public override bool CanTimeout
         {
-            get { return true; }
+            get { return false; }
         }
 
-        /// <summary>
-        /// Gets the length in bytes of the stream.
-        /// </summary>
-        /// <value>A long value representing the length of the stream in bytes.</value>
-        /// <exception cref="NotSupportedException">A class derived from Stream does not support seeking. </exception>
-        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed. </exception>
-        /// <exception cref="IOException">IO operation failed. </exception>
+        /// <inheritdoc/>
         public override long Length
         {
             get
             {
-                // Lock down the file stream while we do this.
-                lock (_lock)
-                {
-                    CheckSessionIsOpen();
+                ThrowIfNotSeekable();
 
-                    if (!CanSeek)
-                    {
-                        throw new NotSupportedException("Seek operation is not supported.");
-                    }
+                Flush();
 
-                    // Flush the write buffer, because it may
-                    // affect the length of the stream.
-                    if (_bufferOwnedByWrite)
-                    {
-                        FlushWriteBuffer();
-                    }
+                var size = _session.RequestFStat(Handle).Size;
 
-                    // obtain file attributes
-                    var attributes = _session.RequestFStat(_handle, nullOnError: true);
-                    if (attributes != null)
-                    {
-                        return attributes.Size;
-                    }
+                Debug.Assert(size >= 0, "fstat should return size as checked in ctor");
 
-                    throw new IOException("Seek operation failed.");
-                }
+                return size;
             }
         }
 
-        /// <summary>
-        /// Gets or sets the position within the current stream.
-        /// </summary>
-        /// <value>The current position within the stream.</value>
-        /// <exception cref="IOException">An I/O error occurs. </exception>
-        /// <exception cref="NotSupportedException">The stream does not support seeking. </exception>
-        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed. </exception>
+        /// <inheritdoc/>
         public override long Position
         {
             get
             {
-                CheckSessionIsOpen();
-
-                if (!CanSeek)
-                {
-                    throw new NotSupportedException("Seek operation not supported.");
-                }
+                ThrowIfNotSeekable();
 
                 return _position;
             }
@@ -155,7 +100,7 @@ namespace Renci.SshNet.Sftp
         /// <value>
         /// The name of the path that was used to construct the current <see cref="SftpFileStream"/>.
         /// </value>
-        public string Name { get; private set; }
+        public string Name { get; }
 
         /// <summary>
         /// Gets the operating system file handle for the file that the current <see cref="SftpFileStream"/> encapsulates.
@@ -163,14 +108,7 @@ namespace Renci.SshNet.Sftp
         /// <value>
         /// The operating system file handle for the file that the current <see cref="SftpFileStream"/> encapsulates.
         /// </value>
-        public byte[] Handle
-        {
-            get
-            {
-                Flush();
-                return _handle;
-            }
-        }
+        public byte[] Handle { get; }
 
         /// <summary>
         /// Gets or sets the operation timeout.
@@ -178,6 +116,7 @@ namespace Renci.SshNet.Sftp
         /// <value>
         /// The timeout.
         /// </value>
+        [EditorBrowsable(EditorBrowsableState.Never)] // Unused
         public TimeSpan Timeout
         {
             get
@@ -192,33 +131,63 @@ namespace Renci.SshNet.Sftp
             }
         }
 
-        private SftpFileStream(ISftpSession session, string path, FileAccess access, int readBufferSize, int writeBufferSize, byte[] handle, long position)
+        private SftpFileStream(
+            ISftpSession session,
+            string path,
+            FileAccess access,
+            bool canSeek,
+            int readBufferSize,
+            int writeBufferSize,
+            byte[] handle,
+            long position,
+            SftpFileReader? initialReader)
         {
             Timeout = TimeSpan.FromSeconds(30);
             Name = path;
 
             _session = session;
-            _canRead = (access & FileAccess.Read) == FileAccess.Read;
-            _canSeek = true;
-            _canWrite = (access & FileAccess.Write) == FileAccess.Write;
+            _access = access;
+            _canSeek = canSeek;
 
-            _handle = handle;
+            Handle = handle;
             _readBufferSize = readBufferSize;
-            _writeBufferSize = writeBufferSize;
             _position = position;
+            _writeBuffer = new System.Net.ArrayBuffer(writeBufferSize);
+            _sftpFileReader = initialReader;
         }
 
-        internal static SftpFileStream Open(ISftpSession session, string path, FileMode mode, FileAccess access, int bufferSize)
+        internal static SftpFileStream Open(
+            ISftpSession? session,
+            string path,
+            FileMode mode,
+            FileAccess access,
+            int bufferSize,
+            bool isDownloadFile = false)
         {
-            return Open(session, path, mode, access, bufferSize, isAsync: false, CancellationToken.None).GetAwaiter().GetResult();
+            return Open(session, path, mode, access, bufferSize, isDownloadFile, isAsync: false, CancellationToken.None).GetAwaiter().GetResult();
         }
 
-        internal static Task<SftpFileStream> OpenAsync(ISftpSession session, string path, FileMode mode, FileAccess access, int bufferSize, CancellationToken cancellationToken)
+        internal static Task<SftpFileStream> OpenAsync(
+            ISftpSession? session,
+            string path,
+            FileMode mode,
+            FileAccess access,
+            int bufferSize,
+            CancellationToken cancellationToken,
+            bool isDownloadFile = false)
         {
-            return Open(session, path, mode, access, bufferSize, isAsync: true, cancellationToken);
+            return Open(session, path, mode, access, bufferSize, isDownloadFile, isAsync: true, cancellationToken);
         }
 
-        private static async Task<SftpFileStream> Open(ISftpSession session, string path, FileMode mode, FileAccess access, int bufferSize, bool isAsync, CancellationToken cancellationToken)
+        private static async Task<SftpFileStream> Open(
+            ISftpSession? session,
+            string path,
+            FileMode mode,
+            FileAccess access,
+            int bufferSize,
+            bool isDownloadFile,
+            bool isAsync,
+            CancellationToken cancellationToken)
         {
             Debug.Assert(isAsync || cancellationToken == default);
 
@@ -234,44 +203,27 @@ namespace Renci.SshNet.Sftp
                 throw new SshConnectionException("Client not connected.");
             }
 
-            var flags = Flags.None;
-
-            switch (access)
+            var flags = access switch
             {
-                case FileAccess.Read:
-                    flags |= Flags.Read;
-                    break;
-                case FileAccess.Write:
-                    flags |= Flags.Write;
-                    break;
-                case FileAccess.ReadWrite:
-                    flags |= Flags.Read;
-                    flags |= Flags.Write;
-                    break;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(access));
-            }
+                FileAccess.Read => Flags.Read,
+                FileAccess.Write => Flags.Write,
+                FileAccess.ReadWrite => Flags.Read | Flags.Write,
+                _ => throw new ArgumentOutOfRangeException(nameof(access))
+            };
 
-            if ((access & FileAccess.Read) == FileAccess.Read && mode == FileMode.Append)
+            if (mode == FileMode.Append && access != FileAccess.Write)
             {
-                throw new ArgumentException(string.Format(CultureInfo.InvariantCulture,
-                                                          "{0} mode can be requested only when combined with write-only access.",
-                                                          mode.ToString("G")),
-                                            nameof(mode));
+                throw new ArgumentException(
+                    "Append mode can be requested only with write-only access.",
+                    nameof(access));
             }
 
-            if ((access & FileAccess.Write) != FileAccess.Write)
+            if (access == FileAccess.Read &&
+                mode is FileMode.Create or FileMode.CreateNew or FileMode.Truncate or FileMode.Append)
             {
-                if (mode is FileMode.Create or FileMode.CreateNew or FileMode.Truncate or FileMode.Append)
-                {
-                    throw new ArgumentException(string.Format(CultureInfo.InvariantCulture,
-                                                              "Combining {0}: {1} with {2}: {3} is invalid.",
-                                                              nameof(FileMode),
-                                                              mode,
-                                                              nameof(FileAccess),
-                                                              access),
-                                                nameof(mode));
-                }
+                throw new ArgumentException(
+                    $"Combining {nameof(FileMode)}: {mode} with {nameof(FileAccess)}: {access} is invalid.",
+                    nameof(access));
             }
 
             switch (mode)
@@ -317,869 +269,527 @@ namespace Renci.SshNet.Sftp
             var readBufferSize = (int)session.CalculateOptimalReadLength((uint)bufferSize);
             var writeBufferSize = (int)session.CalculateOptimalWriteLength((uint)bufferSize, handle);
 
-            long position = 0;
-            if (mode == FileMode.Append)
-            {
-                SftpFileAttributes attributes;
+            SftpFileAttributes? attributes;
 
+            try
+            {
                 if (isAsync)
                 {
                     attributes = await session.RequestFStatAsync(handle, cancellationToken).ConfigureAwait(false);
                 }
                 else
                 {
-                    attributes = session.RequestFStat(handle, nullOnError: false);
+                    attributes = session.RequestFStat(handle);
                 }
+            }
+            catch (SshException ex)
+            {
+                session.SessionLoggerFactory.CreateLogger<SftpFileStream>().LogInformation(
+                    ex, "fstat failed after opening {Path}. Will set CanSeek=false.", path);
 
-                position = attributes.Size;
+                attributes = null;
             }
 
-            return new SftpFileStream(session, path, access, readBufferSize, writeBufferSize, handle, position);
-        }
+            bool canSeek;
+            long position = 0;
+            SftpFileReader? initialReader = null;
 
-        /// <summary>
-        /// Clears all buffers for this stream and causes any buffered data to be written to the file.
-        /// </summary>
-        /// <exception cref="IOException">An I/O error occurs. </exception>
-        /// <exception cref="ObjectDisposedException">Stream is closed.</exception>
-        public override void Flush()
-        {
-            lock (_lock)
+            if (attributes?.Size >= 0)
             {
-                CheckSessionIsOpen();
+                canSeek = true;
 
-                if (_bufferOwnedByWrite)
+                if (mode == FileMode.Append)
                 {
-                    FlushWriteBuffer();
+                    position = attributes.Size;
                 }
-                else
+                else if (isDownloadFile)
+                {
+                    // If we are in a call to SftpClient.DownloadFile, then we know that we will read the whole file,
+                    // so we can let there be several in-flight requests from the get go.
+                    // This optimisation is mostly only beneficial to smaller files on higher latency connections.
+                    // The +2 is +1 for rounding up to cover the whole file, and +1 for the final request to receive EOF.
+                    var initialPendingReads = (int)Math.Max(1, Math.Min(MaxPendingReads, 2 + (attributes.Size / readBufferSize)));
+
+                    initialReader = new(handle, session, readBufferSize, position, MaxPendingReads, (ulong)attributes.Size, initialPendingReads);
+                }
+                else if ((access & FileAccess.Read) == FileAccess.Read)
                 {
-                    FlushReadBuffer();
+                    // The reader can use the size information to reduce in-flight requests near the expected EOF,
+                    // so pass it in here.
+                    initialReader = new(handle, session, readBufferSize, position, MaxPendingReads, (ulong)attributes.Size);
                 }
             }
-        }
-
-        /// <summary>
-        /// Asynchronously clears all buffers for this stream and causes any buffered data to be written to the file.
-        /// </summary>
-        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
-        /// <returns>A <see cref="Task"/> that represents the asynchronous flush operation.</returns>
-        /// <exception cref="IOException">An I/O error occurs. </exception>
-        /// <exception cref="ObjectDisposedException">Stream is closed.</exception>
-        public override Task FlushAsync(CancellationToken cancellationToken)
-        {
-            CheckSessionIsOpen();
-
-            if (_bufferOwnedByWrite)
+            else
             {
-                return FlushWriteBufferAsync(cancellationToken);
+                // Either fstat is failing or it doesn't return the size, in which case we can't support Length,
+                // so CanSeek must return false.
+                canSeek = false;
             }
 
-            FlushReadBuffer();
-
-            return Task.CompletedTask;
+            return new SftpFileStream(session, path, access, canSeek, readBufferSize, writeBufferSize, handle, position, initialReader);
         }
 
-        /// <summary>
-        /// Reads a sequence of bytes from the current stream and advances the position within the stream by the
-        /// number of bytes read.
-        /// </summary>
-        /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="offset"/> and (<paramref name="offset"/> + <paramref name="count"/> - 1) replaced by the bytes read from the current source.</param>
-        /// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream.</param>
-        /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
-        /// <returns>
-        /// The total number of bytes read into the buffer. This can be less than the number of bytes requested
-        /// if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.
-        /// </returns>
-        /// <exception cref="ArgumentException">The sum of <paramref name="offset"/> and <paramref name="count"/> is larger than the buffer length.</exception>
-        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is <see langword="null"/>. </exception>
-        /// <exception cref="ArgumentOutOfRangeException"><paramref name="offset"/> or <paramref name="count"/> is negative.</exception>
-        /// <exception cref="IOException">An I/O error occurs. </exception>
-        /// <exception cref="NotSupportedException">The stream does not support reading. </exception>
-        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed. </exception>
-        /// <remarks>
-        /// <para>
-        /// This method attempts to read up to <paramref name="count"/> bytes. This either from the buffer, from the
-        /// server (using one or more <c>SSH_FXP_READ</c> requests) or using a combination of both.
-        /// </para>
-        /// <para>
-        /// The read loop is interrupted when either <paramref name="count"/> bytes are read, the server returns zero
-        /// bytes (EOF) or less bytes than the read buffer size.
-        /// </para>
-        /// <para>
-        /// When a server returns less number of bytes than the read buffer size, this <c>may</c> indicate that EOF has
-        /// been reached. A subsequent (<c>SSH_FXP_READ</c>) server request is necessary to make sure EOF has effectively
-        /// been reached.  Breaking out of the read loop avoids reading from the server twice to determine EOF: once in
-        /// the read loop, and once upon the next <see cref="Read"/> or <see cref="ReadByte"/> invocation.
-        /// </para>
-        /// </remarks>
-        public override int Read(byte[] buffer, int offset, int count)
+        /// <inheritdoc/>
+        public override void Flush()
         {
-#if !NET
-            ThrowHelper.
-#endif
-            ValidateBufferArguments(buffer, offset, count);
+            ThrowHelper.ThrowObjectDisposedIf(_disposed, this);
 
-            var readLen = 0;
+            var writeLength = _writeBuffer.ActiveLength;
 
-            // Lock down the file stream while we do this.
-            lock (_lock)
+            if (writeLength == 0)
             {
-                CheckSessionIsOpen();
-
-                // Set up for the read operation.
-                SetupRead();
-
-                // Read data into the caller's buffer.
-                while (count > 0)
-                {
-                    // How much data do we have available in the buffer?
-                    var bytesAvailableInBuffer = _bufferLen - _bufferPosition;
-                    if (bytesAvailableInBuffer <= 0)
-                    {
-                        var data = _session.RequestRead(_handle, (ulong)_position, (uint)_readBufferSize);
-
-                        if (data.Length == 0)
-                        {
-                            _bufferPosition = 0;
-                            _bufferLen = 0;
-
-                            break;
-                        }
-
-                        var bytesToWriteToCallerBuffer = count;
-                        if (bytesToWriteToCallerBuffer >= data.Length)
-                        {
-                            // write all data read to caller-provided buffer
-                            bytesToWriteToCallerBuffer = data.Length;
-
-                            // reset buffer since we will skip buffering
-                            _bufferPosition = 0;
-                            _bufferLen = 0;
-                        }
-                        else
-                        {
-                            // determine number of bytes that we should write into read buffer
-                            var bytesToWriteToReadBuffer = data.Length - bytesToWriteToCallerBuffer;
-
-                            // write remaining bytes to read buffer
-                            Buffer.BlockCopy(data, count, GetOrCreateReadBuffer(), 0, bytesToWriteToReadBuffer);
-
-                            // update position in read buffer
-                            _bufferPosition = 0;
-
-                            // update number of bytes in read buffer
-                            _bufferLen = bytesToWriteToReadBuffer;
-                        }
-
-                        // write bytes to caller-provided buffer
-                        Buffer.BlockCopy(data, 0, buffer, offset, bytesToWriteToCallerBuffer);
-
-                        // update stream position
-                        _position += bytesToWriteToCallerBuffer;
-
-                        // record total number of bytes read into caller-provided buffer
-                        readLen += bytesToWriteToCallerBuffer;
+                return;
+            }
 
-                        // break out of the read loop when the server returned less than the request number of bytes
-                        // as that *may* indicate that we've reached EOF
-                        //
-                        // doing this avoids reading from server twice to determine EOF: once in the read loop, and
-                        // once upon the next Read or ReadByte invocation by the caller
-                        if (data.Length < _readBufferSize)
-                        {
-                            break;
-                        }
+            // Under normal usage the offset will be nonnegative, but we nevertheless
+            // perform a checked conversion to prevent writing to a very large offset
+            // in case of corruption due to e.g. invalid multithreaded usage.
+            var serverOffset = checked((ulong)(_position - writeLength));
 
-                        // advance offset to start writing bytes into caller-provided buffer
-                        offset += bytesToWriteToCallerBuffer;
+            using (var wait = new AutoResetEvent(initialState: false))
+            {
+                _session.RequestWrite(
+                    Handle,
+                    serverOffset,
+                    _writeBuffer.DangerousGetUnderlyingBuffer(),
+                    _writeBuffer.ActiveStartOffset,
+                    writeLength,
+                    wait);
 
-                        // update number of bytes left to read into caller-provided buffer
-                        count -= bytesToWriteToCallerBuffer;
-                    }
-                    else
-                    {
-                        // limit the number of bytes to use from read buffer to the caller-request number of bytes
-                        if (bytesAvailableInBuffer > count)
-                        {
-                            bytesAvailableInBuffer = count;
-                        }
+                _writeBuffer.Discard(writeLength);
+            }
+        }
 
-                        // copy data from read buffer to the caller-provided buffer
-                        Buffer.BlockCopy(GetOrCreateReadBuffer(), _bufferPosition, buffer, offset, bytesAvailableInBuffer);
+        /// <inheritdoc/>
+        public override async Task FlushAsync(CancellationToken cancellationToken)
+        {
+            ThrowHelper.ThrowObjectDisposedIf(_disposed, this);
 
-                        // update position in read buffer
-                        _bufferPosition += bytesAvailableInBuffer;
+            var writeLength = _writeBuffer.ActiveLength;
 
-                        // update stream position
-                        _position += bytesAvailableInBuffer;
+            if (writeLength == 0)
+            {
+                return;
+            }
 
-                        // record total number of bytes read into caller-provided buffer
-                        readLen += bytesAvailableInBuffer;
+            // Under normal usage the offset will be nonnegative, but we nevertheless
+            // perform a checked conversion to prevent writing to a very large offset
+            // in case of corruption due to e.g. invalid multithreaded usage.
+            var serverOffset = checked((ulong)(_position - writeLength));
 
-                        // advance offset to start writing bytes into caller-provided buffer
-                        offset += bytesAvailableInBuffer;
+            await _session.RequestWriteAsync(
+                Handle,
+                serverOffset,
+                _writeBuffer.DangerousGetUnderlyingBuffer(),
+                _writeBuffer.ActiveStartOffset,
+                writeLength,
+                cancellationToken).ConfigureAwait(false);
 
-                        // update number of bytes left to read
-                        count -= bytesAvailableInBuffer;
-                    }
-                }
-            }
+            _writeBuffer.Discard(writeLength);
+        }
 
-            // return the number of bytes that were read to the caller.
-            return readLen;
+        private void InvalidateReads()
+        {
+            _readBuffer = ReadOnlyMemory<byte>.Empty;
+            _sftpFileReader?.Dispose();
+            _sftpFileReader = null;
         }
 
-        /// <summary>
-        /// Asynchronously reads a sequence of bytes from the current stream and advances the position within the stream by the
-        /// number of bytes read.
-        /// </summary>
-        /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="offset"/> and (<paramref name="offset"/> + <paramref name="count"/> - 1) replaced by the bytes read from the current source.</param>
-        /// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream.</param>
-        /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
-        /// <param name="cancellationToken">The <see cref="CancellationToken" /> to observe.</param>
-        /// <returns>A <see cref="Task" /> that represents the asynchronous read operation.</returns>
-        public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        /// <inheritdoc/>
+        public override int Read(byte[] buffer, int offset, int count)
         {
 #if !NET
             ThrowHelper.
 #endif
             ValidateBufferArguments(buffer, offset, count);
 
-            cancellationToken.ThrowIfCancellationRequested();
-
-            var readLen = 0;
-
-            CheckSessionIsOpen();
+            return Read(buffer.AsSpan(offset, count));
+        }
 
-            // Set up for the read operation.
-            SetupRead();
+#if NET
+        /// <inheritdoc/>
+        public override int Read(Span<byte> buffer)
+#else
+        private int Read(Span<byte> buffer)
+#endif
+        {
+            ThrowIfNotReadable();
 
-            // Read data into the caller's buffer.
-            while (count > 0)
+            if (_readBuffer.IsEmpty)
             {
-                // How much data do we have available in the buffer?
-                var bytesAvailableInBuffer = _bufferLen - _bufferPosition;
-                if (bytesAvailableInBuffer <= 0)
+                if (_sftpFileReader is null)
                 {
-                    var data = await _session.RequestReadAsync(_handle, (ulong)_position, (uint)_readBufferSize, cancellationToken).ConfigureAwait(false);
+                    Flush();
+                    _sftpFileReader = new(Handle, _session, _readBufferSize, _position, MaxPendingReads);
+                }
 
-                    if (data.Length == 0)
-                    {
-                        _bufferPosition = 0;
-                        _bufferLen = 0;
+                _readBuffer = _sftpFileReader.ReadAsync(CancellationToken.None).GetAwaiter().GetResult();
 
-                        break;
-                    }
-
-                    var bytesToWriteToCallerBuffer = count;
-                    if (bytesToWriteToCallerBuffer >= data.Length)
-                    {
-                        // write all data read to caller-provided buffer
-                        bytesToWriteToCallerBuffer = data.Length;
+                if (_readBuffer.IsEmpty)
+                {
+                    // If we've hit EOF then throw away this reader instance.
+                    // If Read is called again we will create a new reader.
+                    // This takes care of the case when a file is expanding
+                    // during reading.
+                    _sftpFileReader.Dispose();
+                    _sftpFileReader = null;
+                }
+            }
 
-                        // reset buffer since we will skip buffering
-                        _bufferPosition = 0;
-                        _bufferLen = 0;
-                    }
-                    else
-                    {
-                        // determine number of bytes that we should write into read buffer
-                        var bytesToWriteToReadBuffer = data.Length - bytesToWriteToCallerBuffer;
+            Debug.Assert(_writeBuffer.ActiveLength == 0, "Write buffer should be empty when reading.");
 
-                        // write remaining bytes to read buffer
-                        Buffer.BlockCopy(data, count, GetOrCreateReadBuffer(), 0, bytesToWriteToReadBuffer);
+            var bytesRead = Math.Min(buffer.Length, _readBuffer.Length);
 
-                        // update position in read buffer
-                        _bufferPosition = 0;
+            _readBuffer.Span.Slice(0, bytesRead).CopyTo(buffer);
+            _readBuffer = _readBuffer.Slice(bytesRead);
 
-                        // update number of bytes in read buffer
-                        _bufferLen = bytesToWriteToReadBuffer;
-                    }
+            _position += bytesRead;
 
-                    // write bytes to caller-provided buffer
-                    Buffer.BlockCopy(data, 0, buffer, offset, bytesToWriteToCallerBuffer);
+            return bytesRead;
+        }
 
-                    // update stream position
-                    _position += bytesToWriteToCallerBuffer;
+        /// <inheritdoc/>
+        public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        {
+#if !NET
+            ThrowHelper.
+#endif
+            ValidateBufferArguments(buffer, offset, count);
 
-                    // record total number of bytes read into caller-provided buffer
-                    readLen += bytesToWriteToCallerBuffer;
+            return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
+        }
 
-                    // break out of the read loop when the server returned less than the request number of bytes
-                    // as that *may* indicate that we've reached EOF
-                    //
-                    // doing this avoids reading from server twice to determine EOF: once in the read loop, and
-                    // once upon the next Read or ReadByte invocation by the caller
-                    if (data.Length < _readBufferSize)
-                    {
-                        break;
-                    }
+#if NET
+        /// <inheritdoc/>
+        public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+#else
+        private async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
+#endif
+        {
+            ThrowIfNotReadable();
 
-                    // advance offset to start writing bytes into caller-provided buffer
-                    offset += bytesToWriteToCallerBuffer;
+            if (_readBuffer.IsEmpty)
+            {
+                if (_sftpFileReader is null)
+                {
+                    await FlushAsync(cancellationToken).ConfigureAwait(false);
 
-                    // update number of bytes left to read into caller-provided buffer
-                    count -= bytesToWriteToCallerBuffer;
+                    _sftpFileReader = new(Handle, _session, _readBufferSize, _position, MaxPendingReads);
                 }
-                else
-                {
-                    // limit the number of bytes to use from read buffer to the caller-request number of bytes
-                    if (bytesAvailableInBuffer > count)
-                    {
-                        bytesAvailableInBuffer = count;
-                    }
 
-                    // copy data from read buffer to the caller-provided buffer
-                    Buffer.BlockCopy(GetOrCreateReadBuffer(), _bufferPosition, buffer, offset, bytesAvailableInBuffer);
+                _readBuffer = await _sftpFileReader.ReadAsync(cancellationToken).ConfigureAwait(false);
 
-                    // update position in read buffer
-                    _bufferPosition += bytesAvailableInBuffer;
+                if (_readBuffer.IsEmpty)
+                {
+                    // If we've hit EOF then throw away this reader instance.
+                    // If Read is called again we will create a new reader.
+                    // This takes care of the case when a file is expanding
+                    // during reading.
+                    _sftpFileReader.Dispose();
+                    _sftpFileReader = null;
+                }
+            }
 
-                    // update stream position
-                    _position += bytesAvailableInBuffer;
+            Debug.Assert(_writeBuffer.ActiveLength == 0, "Write buffer should be empty when reading.");
 
-                    // record total number of bytes read into caller-provided buffer
-                    readLen += bytesAvailableInBuffer;
+            var bytesRead = Math.Min(buffer.Length, _readBuffer.Length);
 
-                    // advance offset to start writing bytes into caller-provided buffer
-                    offset += bytesAvailableInBuffer;
+            _readBuffer.Slice(0, bytesRead).CopyTo(buffer);
+            _readBuffer = _readBuffer.Slice(bytesRead);
 
-                    // update number of bytes left to read
-                    count -= bytesAvailableInBuffer;
-                }
-            }
+            _position += bytesRead;
 
-            // return the number of bytes that were read to the caller.
-            return readLen;
+            return bytesRead;
         }
 
-        /// <summary>
-        /// Reads a byte from the stream and advances the position within the stream by one byte, or returns -1 if at the end of the stream.
-        /// </summary>
-        /// <returns>
-        /// The unsigned byte cast to an <see cref="int"/>, or -1 if at the end of the stream.
-        /// </returns>
-        /// <exception cref="NotSupportedException">The stream does not support reading. </exception>
-        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed. </exception>
-        /// <exception cref="IOException">Read operation failed.</exception>
+#if NET
+        /// <inheritdoc/>
         public override int ReadByte()
         {
-            // Lock down the file stream while we do this.
-            lock (_lock)
-            {
-                CheckSessionIsOpen();
-
-                // Setup the object for reading.
-                SetupRead();
-
-                byte[] readBuffer;
-
-                // Read more data into the internal buffer if necessary.
-                if (_bufferPosition >= _bufferLen)
-                {
-                    var data = _session.RequestRead(_handle, (ulong)_position, (uint)_readBufferSize);
-                    if (data.Length == 0)
-                    {
-                        // We've reached EOF.
-                        return -1;
-                    }
+            byte b = default;
+            var read = Read(new Span<byte>(ref b));
+            return read == 0 ? -1 : b;
+        }
+#endif
 
-                    readBuffer = GetOrCreateReadBuffer();
-                    Buffer.BlockCopy(data, 0, readBuffer, 0, data.Length);
+        /// <inheritdoc/>
+        public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
+        {
+            return TaskToAsyncResult.Begin(ReadAsync(buffer, offset, count), callback, state);
+        }
 
-                    _bufferPosition = 0;
-                    _bufferLen = data.Length;
-                }
-                else
-                {
-                    readBuffer = GetOrCreateReadBuffer();
-                }
+        /// <inheritdoc/>
+        public override int EndRead(IAsyncResult asyncResult)
+        {
+            return TaskToAsyncResult.End<int>(asyncResult);
+        }
 
-                // Extract the next byte from the buffer.
-                ++_position;
+        /// <inheritdoc/>
+        public override void Write(byte[] buffer, int offset, int count)
+        {
+#if !NET
+            ThrowHelper.
+#endif
+            ValidateBufferArguments(buffer, offset, count);
 
-                return readBuffer[_bufferPosition++];
-            }
+            Write(buffer.AsSpan(offset, count));
         }
 
-        /// <summary>
-        /// Sets the position within the current stream.
-        /// </summary>
-        /// <param name="offset">A byte offset relative to the <paramref name="origin"/> parameter.</param>
-        /// <param name="origin">A value of type <see cref="SeekOrigin"/> indicating the reference point used to obtain the new position.</param>
-        /// <returns>
-        /// The new position within the current stream.
-        /// </returns>
-        /// <exception cref="IOException">An I/O error occurs. </exception>
-        /// <exception cref="NotSupportedException">The stream does not support seeking, such as if the stream is constructed from a pipe or console output. </exception>
-        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed. </exception>
-        public override long Seek(long offset, SeekOrigin origin)
+#if NET
+        /// <inheritdoc/>
+        public override void Write(ReadOnlySpan<byte> buffer)
+#else
+        private void Write(ReadOnlySpan<byte> buffer)
+#endif
         {
-            long newPosn;
+            ThrowIfNotWriteable();
 
-            // Lock down the file stream while we do this.
-            lock (_lock)
-            {
-                CheckSessionIsOpen();
+            InvalidateReads();
 
-                if (!CanSeek)
-                {
-                    throw new NotSupportedException("Seek is not supported.");
-                }
-
-                // Don't do anything if the position won't be moving.
-                if (origin == SeekOrigin.Begin && offset == _position)
-                {
-                    return offset;
-                }
+            while (!buffer.IsEmpty)
+            {
+                var byteCount = Math.Min(buffer.Length, _writeBuffer.AvailableLength);
 
-                if (origin == SeekOrigin.Current && offset == 0)
-                {
-                    return _position;
-                }
+                buffer.Slice(0, byteCount).CopyTo(_writeBuffer.AvailableSpan);
 
-                // The behaviour depends upon the read/write mode.
-                if (_bufferOwnedByWrite)
-                {
-                    // Flush the write buffer and then seek.
-                    FlushWriteBuffer();
-                }
-                else
-                {
-                    // Determine if the seek is to somewhere inside
-                    // the current read buffer bounds.
-                    if (origin == SeekOrigin.Begin)
-                    {
-                        newPosn = _position - _bufferPosition;
-                        if (offset >= newPosn && offset < (newPosn + _bufferLen))
-                        {
-                            _bufferPosition = (int)(offset - newPosn);
-                            _position = offset;
-                            return _position;
-                        }
-                    }
-                    else if (origin == SeekOrigin.Current)
-                    {
-                        newPosn = _position + offset;
-                        if (newPosn >= (_position - _bufferPosition) &&
-                           newPosn < (_position - _bufferPosition + _bufferLen))
-                        {
-                            _bufferPosition = (int)(newPosn - (_position - _bufferPosition));
-                            _position = newPosn;
-                            return _position;
-                        }
-                    }
+                buffer = buffer.Slice(byteCount);
 
-                    // Abandon the read buffer.
-                    _bufferPosition = 0;
-                    _bufferLen = 0;
-                }
+                _writeBuffer.Commit(byteCount);
 
-                // Seek to the new position.
-                switch (origin)
-                {
-                    case SeekOrigin.Begin:
-                        newPosn = offset;
-                        break;
-                    case SeekOrigin.Current:
-                        newPosn = _position + offset;
-                        break;
-                    case SeekOrigin.End:
-                        var attributes = _session.RequestFStat(_handle, nullOnError: false);
-                        newPosn = attributes.Size + offset;
-                        break;
-                    default:
-                        throw new ArgumentException("Invalid seek origin.", nameof(origin));
-                }
+                _position += byteCount;
 
-                if (newPosn < 0)
+                if (_writeBuffer.AvailableLength == 0)
                 {
-                    throw new EndOfStreamException();
+                    Flush();
                 }
-
-                _position = newPosn;
-                return _position;
             }
         }
 
-        /// <summary>
-        /// Sets the length of the current stream.
-        /// </summary>
-        /// <param name="value">The desired length of the current stream in bytes.</param>
-        /// <exception cref="IOException">An I/O error occurs.</exception>
-        /// <exception cref="NotSupportedException">The stream does not support both writing and seeking.</exception>
-        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
-        /// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> must be greater than zero.</exception>
-        /// <remarks>
-        /// <para>
-        /// Buffers are first flushed.
-        /// </para>
-        /// <para>
-        /// If the specified value is less than the current length of the stream, the stream is truncated and - if the
-        /// current position is greater than the new length - the current position is moved to the last byte of the stream.
-        /// </para>
-        /// <para>
-        /// If the given value is greater than the current length of the stream, the stream is expanded and the current
-        /// position remains the same.
-        /// </para>
-        /// </remarks>
-        public override void SetLength(long value)
+        /// <inheritdoc/>
+        public override void WriteByte(byte value)
         {
-            ThrowHelper.ThrowIfNegative(value);
-
-            // Lock down the file stream while we do this.
-            lock (_lock)
-            {
-                CheckSessionIsOpen();
-
-                if (!CanSeek)
-                {
-                    throw new NotSupportedException("Seek is not supported.");
-                }
-
-                if (_bufferOwnedByWrite)
-                {
-                    FlushWriteBuffer();
-                }
-                else
-                {
-                    SetupWrite();
-                }
-
-                var attributes = _session.RequestFStat(_handle, nullOnError: false);
-                attributes.Size = value;
-                _session.RequestFSetStat(_handle, attributes);
-
-                if (_position > value)
-                {
-                    _position = value;
-                }
-            }
+            Write([value]);
         }
 
-        /// <summary>
-        /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
-        /// </summary>
-        /// <param name="buffer">An array of bytes. This method copies <paramref name="count"/> bytes from <paramref name="buffer"/> to the current stream.</param>
-        /// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin copying bytes to the current stream.</param>
-        /// <param name="count">The number of bytes to be written to the current stream.</param>
-        /// <exception cref="ArgumentException">The sum of <paramref name="offset"/> and <paramref name="count"/> is greater than the buffer length.</exception>
-        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is <see langword="null"/>.</exception>
-        /// <exception cref="ArgumentOutOfRangeException"><paramref name="offset"/> or <paramref name="count"/> is negative.</exception>
-        /// <exception cref="IOException">An I/O error occurs.</exception>
-        /// <exception cref="NotSupportedException">The stream does not support writing.</exception>
-        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
-        public override void Write(byte[] buffer, int offset, int count)
+        /// <inheritdoc/>
+        public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
         {
 #if !NET
             ThrowHelper.
 #endif
             ValidateBufferArguments(buffer, offset, count);
 
-            // Lock down the file stream while we do this.
-            lock (_lock)
-            {
-                CheckSessionIsOpen();
+            return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
+        }
 
-                // Setup this object for writing.
-                SetupWrite();
+#if NET
+        /// <inheritdoc/>
+        public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
+#else
+        private async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
+#endif
+        {
+            ThrowIfNotWriteable();
 
-                // Write data to the file stream.
-                while (count > 0)
-                {
-                    // Determine how many bytes we can write to the buffer.
-                    var tempLen = _writeBufferSize - _bufferPosition;
-                    if (tempLen <= 0)
-                    {
-                        // flush write buffer, and mark it empty
-                        FlushWriteBuffer();
+            InvalidateReads();
 
-                        // we can now write or buffer the full buffer size
-                        tempLen = _writeBufferSize;
-                    }
+            while (!buffer.IsEmpty)
+            {
+                var byteCount = Math.Min(buffer.Length, _writeBuffer.AvailableLength);
 
-                    // limit the number of bytes to write to the actual number of bytes requested
-                    if (tempLen > count)
-                    {
-                        tempLen = count;
-                    }
+                buffer.Slice(0, byteCount).CopyTo(_writeBuffer.AvailableMemory);
 
-                    // Can we short-cut the internal buffer?
-                    if (_bufferPosition == 0 && tempLen == _writeBufferSize)
-                    {
-                        using (var wait = new AutoResetEvent(initialState: false))
-                        {
-                            _session.RequestWrite(_handle, (ulong)_position, buffer, offset, tempLen, wait);
-                        }
-                    }
-                    else
-                    {
-                        // No: copy the data to the write buffer first.
-                        Buffer.BlockCopy(buffer, offset, GetOrCreateWriteBuffer(), _bufferPosition, tempLen);
-                        _bufferPosition += tempLen;
-                    }
+                buffer = buffer.Slice(byteCount);
 
-                    // Advance the buffer and stream positions.
-                    _position += tempLen;
-                    offset += tempLen;
-                    count -= tempLen;
-                }
+                _writeBuffer.Commit(byteCount);
 
-                // If the buffer is full, then do a speculative flush now,
-                // rather than waiting for the next call to this method.
-                if (_bufferPosition >= _writeBufferSize)
-                {
-                    using (var wait = new AutoResetEvent(initialState: false))
-                    {
-                        _session.RequestWrite(_handle, (ulong)(_position - _bufferPosition), GetOrCreateWriteBuffer(), 0, _bufferPosition, wait);
-                    }
+                _position += byteCount;
 
-                    _bufferPosition = 0;
+                if (_writeBuffer.AvailableLength == 0)
+                {
+                    await FlushAsync(cancellationToken).ConfigureAwait(false);
                 }
             }
         }
 
-        /// <summary>
-        /// Asynchronously writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
-        /// </summary>
-        /// <param name="buffer">An array of bytes. This method copies <paramref name="count"/> bytes from <paramref name="buffer"/> to the current stream.</param>
-        /// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin copying bytes to the current stream.</param>
-        /// <param name="count">The number of bytes to be written to the current stream.</param>
-        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
-        /// <returns>A <see cref="Task"/> that represents the asynchronous write operation.</returns>
-        /// <exception cref="ArgumentException">The sum of <paramref name="offset"/> and <paramref name="count"/> is greater than the buffer length.</exception>
-        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is <see langword="null"/>.</exception>
-        /// <exception cref="ArgumentOutOfRangeException"><paramref name="offset"/> or <paramref name="count"/> is negative.</exception>
-        /// <exception cref="IOException">An I/O error occurs.</exception>
-        /// <exception cref="NotSupportedException">The stream does not support writing.</exception>
-        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
-        public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        /// <inheritdoc/>
+        public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
         {
-#if !NET
-            ThrowHelper.
-#endif
-            ValidateBufferArguments(buffer, offset, count);
+            return TaskToAsyncResult.Begin(WriteAsync(buffer, offset, count), callback, state);
+        }
 
-            cancellationToken.ThrowIfCancellationRequested();
+        /// <inheritdoc/>
+        public override void EndWrite(IAsyncResult asyncResult)
+        {
+            TaskToAsyncResult.End(asyncResult);
+        }
 
-            CheckSessionIsOpen();
+        /// <inheritdoc/>
+        public override long Seek(long offset, SeekOrigin origin)
+        {
+            ThrowIfNotSeekable();
 
-            // Setup this object for writing.
-            SetupWrite();
+            Flush();
 
-            // Write data to the file stream.
-            while (count > 0)
+            var newPosition = origin switch
             {
-                // Determine how many bytes we can write to the buffer.
-                var tempLen = _writeBufferSize - _bufferPosition;
-                if (tempLen <= 0)
-                {
-                    // flush write buffer, and mark it empty
-                    await FlushWriteBufferAsync(cancellationToken).ConfigureAwait(false);
-
-                    // we can now write or buffer the full buffer size
-                    tempLen = _writeBufferSize;
-                }
+                SeekOrigin.Begin => offset,
+                SeekOrigin.Current => _position + offset,
+                SeekOrigin.End => _session.RequestFStat(Handle).Size + offset,
+                _ => throw new ArgumentOutOfRangeException(nameof(origin))
+            };
 
-                // limit the number of bytes to write to the actual number of bytes requested
-                if (tempLen > count)
-                {
-                    tempLen = count;
-                }
+            if (newPosition < 0)
+            {
+                throw new IOException("An attempt was made to move the position before the beginning of the stream.");
+            }
 
-                // Can we short-cut the internal buffer?
-                if (_bufferPosition == 0 && tempLen == _writeBufferSize)
-                {
-                    await _session.RequestWriteAsync(_handle, (ulong)_position, buffer, offset, tempLen, cancellationToken).ConfigureAwait(false);
-                }
-                else
-                {
-                    // No: copy the data to the write buffer first.
-                    Buffer.BlockCopy(buffer, offset, GetOrCreateWriteBuffer(), _bufferPosition, tempLen);
-                    _bufferPosition += tempLen;
-                }
+            var readBufferStart = _position; // inclusive
+            var readBufferEnd = _position + _readBuffer.Length; // exclusive
 
-                // Advance the buffer and stream positions.
-                _position += tempLen;
-                offset += tempLen;
-                count -= tempLen;
+            if (readBufferStart <= newPosition && newPosition <= readBufferEnd)
+            {
+                _readBuffer = _readBuffer.Slice((int)(newPosition - readBufferStart));
             }
-
-            // If the buffer is full, then do a speculative flush now,
-            // rather than waiting for the next call to this method.
-            if (_bufferPosition >= _writeBufferSize)
+            else
             {
-                await _session.RequestWriteAsync(_handle, (ulong)(_position - _bufferPosition), GetOrCreateWriteBuffer(), 0, _bufferPosition, cancellationToken).ConfigureAwait(false);
-                _bufferPosition = 0;
+                InvalidateReads();
             }
+
+            return _position = newPosition;
         }
 
-        /// <summary>
-        /// Writes a byte to the current position in the stream and advances the position within the stream by one byte.
-        /// </summary>
-        /// <param name="value">The byte to write to the stream.</param>
-        /// <exception cref="IOException">An I/O error occurs. </exception>
-        /// <exception cref="NotSupportedException">The stream does not support writing, or the stream is already closed. </exception>
-        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed. </exception>
-        public override void WriteByte(byte value)
+        /// <inheritdoc/>
+        public override void SetLength(long value)
         {
-            // Lock down the file stream while we do this.
-            lock (_lock)
-            {
-                CheckSessionIsOpen();
-
-                // Setup the object for writing.
-                SetupWrite();
-
-                var writeBuffer = GetOrCreateWriteBuffer();
+            ThrowHelper.ThrowIfNegative(value);
+            ThrowIfNotWriteable();
+            ThrowIfNotSeekable();
 
-                // Flush the current buffer if it is full.
-                if (_bufferPosition >= _writeBufferSize)
-                {
-                    using (var wait = new AutoResetEvent(initialState: false))
-                    {
-                        _session.RequestWrite(_handle, (ulong)(_position - _bufferPosition), writeBuffer, 0, _bufferPosition, wait);
-                    }
+            Flush();
+            InvalidateReads();
 
-                    _bufferPosition = 0;
-                }
+            var attributes = _session.RequestFStat(Handle);
+            attributes.Size = value;
+            _session.RequestFSetStat(Handle, attributes);
 
-                // Write the byte into the buffer and advance the posn.
-                writeBuffer[_bufferPosition++] = value;
-                ++_position;
+            if (_position > value)
+            {
+                _position = value;
             }
         }
 
-        /// <summary>
-        /// Releases the unmanaged resources used by the <see cref="Stream"/> and optionally releases the managed resources.
-        /// </summary>
-        /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
+        /// <inheritdoc/>
         protected override void Dispose(bool disposing)
         {
-            base.Dispose(disposing);
+            if (_disposed)
+            {
+                return;
+            }
 
-            if (_session != null)
+            try
             {
-                if (disposing)
+                if (disposing && _session.IsOpen)
                 {
-                    lock (_lock)
+                    try
                     {
-                        if (_session != null)
+                        Flush();
+                    }
+                    finally
+                    {
+                        if (_session.IsOpen)
                         {
-                            _canRead = false;
-                            _canSeek = false;
-                            _canWrite = false;
-
-                            if (_handle != null)
-                            {
-                                if (_session.IsOpen)
-                                {
-                                    if (_bufferOwnedByWrite)
-                                    {
-                                        FlushWriteBuffer();
-                                    }
-
-                                    _session.RequestClose(_handle);
-                                }
-
-                                _handle = null;
-                            }
-
-                            _session = null;
+                            _session.RequestClose(Handle);
                         }
                     }
                 }
             }
+            finally
+            {
+                _disposed = true;
+                InvalidateReads();
+                base.Dispose(disposing);
+            }
         }
 
-        private byte[] GetOrCreateReadBuffer()
-        {
-            _readBuffer ??= new byte[_readBufferSize];
-            return _readBuffer;
-        }
-
-        private byte[] GetOrCreateWriteBuffer()
-        {
-            _writeBuffer ??= new byte[_writeBufferSize];
-            return _writeBuffer;
-        }
-
-        /// <summary>
-        /// Flushes the read data from the buffer.
-        /// </summary>
-        private void FlushReadBuffer()
+#if NET
+        /// <inheritdoc/>
+#pragma warning disable CA2215 // Dispose methods should call base class dispose
+        public override async ValueTask DisposeAsync()
+#pragma warning restore CA2215 // Dispose methods should call base class dispose
+#else
+        internal async ValueTask DisposeAsync()
+#endif
         {
-            _bufferPosition = 0;
-            _bufferLen = 0;
-        }
+            if (_disposed)
+            {
+                return;
+            }
 
-        /// <summary>
-        /// Flush any buffered write data to the file.
-        /// </summary>
-        private void FlushWriteBuffer()
-        {
-            if (_bufferPosition > 0)
+            try
             {
-                using (var wait = new AutoResetEvent(initialState: false))
+                if (_session.IsOpen)
                 {
-                    _session.RequestWrite(_handle, (ulong)(_position - _bufferPosition), _writeBuffer, 0, _bufferPosition, wait);
+                    try
+                    {
+                        await FlushAsync().ConfigureAwait(false);
+                    }
+                    finally
+                    {
+                        if (_session.IsOpen)
+                        {
+                            await _session.RequestCloseAsync(Handle, CancellationToken.None).ConfigureAwait(false);
+                        }
+                    }
                 }
-
-                _bufferPosition = 0;
             }
-        }
-
-        private async Task FlushWriteBufferAsync(CancellationToken cancellationToken)
-        {
-            if (_bufferPosition > 0)
+            finally
             {
-                await _session.RequestWriteAsync(_handle, (ulong)(_position - _bufferPosition), _writeBuffer, 0, _bufferPosition, cancellationToken).ConfigureAwait(false);
-                _bufferPosition = 0;
+                _disposed = true;
+                InvalidateReads();
+                base.Dispose(disposing: false);
             }
         }
 
-        /// <summary>
-        /// Setups the read.
-        /// </summary>
-        private void SetupRead()
+        private void ThrowIfNotSeekable()
         {
-            if (!CanRead)
+            if (!CanSeek)
             {
-                throw new NotSupportedException("Read not supported.");
+                ThrowHelper.ThrowObjectDisposedIf(_disposed, this);
+                Throw();
             }
 
-            if (_bufferOwnedByWrite)
+            static void Throw()
             {
-                FlushWriteBuffer();
-                _bufferOwnedByWrite = false;
+                throw new NotSupportedException("Stream does not support seeking.");
             }
         }
 
-        /// <summary>
-        /// Setups the write.
-        /// </summary>
-        private void SetupWrite()
+        private void ThrowIfNotWriteable()
         {
             if (!CanWrite)
             {
-                throw new NotSupportedException("Write not supported.");
+                ThrowHelper.ThrowObjectDisposedIf(_disposed, this);
+                Throw();
             }
 
-            if (!_bufferOwnedByWrite)
+            static void Throw()
             {
-                FlushReadBuffer();
-                _bufferOwnedByWrite = true;
+                throw new NotSupportedException("Stream does not support writing.");
             }
         }
 
-        private void CheckSessionIsOpen()
+        private void ThrowIfNotReadable()
         {
-            ThrowHelper.ThrowObjectDisposedIf(_session is null, this);
+            if (!CanRead)
+            {
+                ThrowHelper.ThrowObjectDisposedIf(_disposed, this);
+                Throw();
+            }
 
-            if (!_session.IsOpen)
+            static void Throw()
             {
-                throw new ObjectDisposedException(GetType().FullName, "Cannot access a closed SFTP session.");
+                throw new NotSupportedException("Stream does not support reading.");
             }
         }
     }

+ 5 - 27
src/Renci.SshNet/Sftp/SftpSession.cs

@@ -251,23 +251,6 @@ namespace Renci.SshNet.Sftp
             return canonizedPath + slash + pathParts[pathParts.Length - 1];
         }
 
-        /// <summary>
-        /// Creates an <see cref="ISftpFileReader"/> for reading the content of the file represented by a given <paramref name="handle"/>.
-        /// </summary>
-        /// <param name="handle">The handle of the file to read.</param>
-        /// <param name="sftpSession">The SFTP session.</param>
-        /// <param name="chunkSize">The maximum number of bytes to read with each chunk.</param>
-        /// <param name="maxPendingReads">The maximum number of pending reads.</param>
-        /// <param name="fileSize">The size of the file or <see langword="null"/> when the size could not be determined.</param>
-        /// <returns>
-        /// An <see cref="ISftpFileReader"/> for reading the content of the file represented by the
-        /// specified <paramref name="handle"/>.
-        /// </returns>
-        public ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize)
-        {
-            return new SftpFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
-        }
-
         internal string GetFullRemotePath(string path)
         {
             var fullPath = path;
@@ -820,6 +803,8 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         public Task<byte[]> RequestReadAsync(byte[] handle, ulong offset, uint length, CancellationToken cancellationToken)
         {
+            Debug.Assert(length > 0, "This implementation cannot distinguish between EOF and zero-length reads");
+
             if (cancellationToken.IsCancellationRequested)
             {
                 return Task.FromCanceled<byte[]>(cancellationToken);
@@ -1075,15 +1060,8 @@ namespace Renci.SshNet.Sftp
             }
         }
 
-        /// <summary>
-        /// Performs SSH_FXP_FSTAT request.
-        /// </summary>
-        /// <param name="handle">The handle.</param>
-        /// <param name="nullOnError">If set to <see langword="true"/>, returns <see langword="null"/> instead of throwing an exception.</param>
-        /// <returns>
-        /// File attributes.
-        /// </returns>
-        public SftpFileAttributes RequestFStat(byte[] handle, bool nullOnError)
+        /// <inheritdoc/>
+        public SftpFileAttributes RequestFStat(byte[] handle)
         {
             SshException exception = null;
             SftpFileAttributes attributes = null;
@@ -1109,7 +1087,7 @@ namespace Renci.SshNet.Sftp
                 WaitOnHandle(wait, OperationTimeout);
             }
 
-            if (!nullOnError && exception is not null)
+            if (exception is not null)
             {
                 throw exception;
             }

+ 122 - 47
src/Renci.SshNet/SftpClient.cs

@@ -1,5 +1,6 @@
 #nullable enable
 using System;
+using System.Buffers;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
@@ -899,17 +900,33 @@ namespace Renci.SshNet
         /// <inheritdoc />
         public void DownloadFile(string path, Stream output, Action<ulong>? downloadCallback = null)
         {
+            ThrowHelper.ThrowIfNullOrWhiteSpace(path);
+            ThrowHelper.ThrowIfNull(output);
             CheckDisposed();
 
-            InternalDownloadFile(path, output, asyncResult: null, downloadCallback);
+            InternalDownloadFile(
+                path,
+                output,
+                asyncResult: null,
+                downloadCallback,
+                isAsync: false,
+                CancellationToken.None).GetAwaiter().GetResult();
         }
 
         /// <inheritdoc />
         public Task DownloadFileAsync(string path, Stream output, CancellationToken cancellationToken = default)
         {
+            ThrowHelper.ThrowIfNullOrWhiteSpace(path);
+            ThrowHelper.ThrowIfNull(output);
             CheckDisposed();
 
-            return InternalDownloadFileAsync(path, output, cancellationToken);
+            return InternalDownloadFile(
+                path,
+                output,
+                asyncResult: null,
+                downloadCallback: null,
+                isAsync: true,
+                cancellationToken);
         }
 
         /// <summary>
@@ -976,17 +993,25 @@ namespace Renci.SshNet
         /// </remarks>
         public IAsyncResult BeginDownloadFile(string path, Stream output, AsyncCallback? asyncCallback, object? state, Action<ulong>? downloadCallback = null)
         {
-            CheckDisposed();
             ThrowHelper.ThrowIfNullOrWhiteSpace(path);
             ThrowHelper.ThrowIfNull(output);
+            CheckDisposed();
 
             var asyncResult = new SftpDownloadAsyncResult(asyncCallback, state);
 
-            ThreadAbstraction.ExecuteThread(() =>
+            _ = DoDownloadAndSetResult();
+
+            async Task DoDownloadAndSetResult()
             {
                 try
                 {
-                    InternalDownloadFile(path, output, asyncResult, downloadCallback);
+                    await InternalDownloadFile(
+                        path,
+                        output,
+                        asyncResult,
+                        downloadCallback,
+                        isAsync: true,
+                        CancellationToken.None).ConfigureAwait(false);
 
                     asyncResult.SetAsCompleted(exception: null, completedSynchronously: false);
                 }
@@ -994,7 +1019,7 @@ namespace Renci.SshNet
                 {
                     asyncResult.SetAsCompleted(exp, completedSynchronously: false);
                 }
-            });
+            }
 
             return asyncResult;
         }
@@ -1050,7 +1075,7 @@ namespace Renci.SshNet
                 asyncResult: null,
                 uploadCallback,
                 isAsync: false,
-                default).GetAwaiter().GetResult();
+                CancellationToken.None).GetAwaiter().GetResult();
         }
 
         /// <inheritdoc />
@@ -1689,8 +1714,20 @@ namespace Renci.SshNet
         {
             using (var stream = OpenRead(path))
             {
-                var buffer = new byte[stream.Length];
-                stream.ReadExactly(buffer, 0, buffer.Length);
+                byte[] buffer;
+
+                if (stream.CanSeek)
+                {
+                    buffer = new byte[stream.Length];
+                    stream.ReadExactly(buffer, 0, buffer.Length);
+                }
+                else
+                {
+                    MemoryStream ms = new();
+                    stream.CopyTo(ms);
+                    buffer = ms.ToArray();
+                }
+
                 return buffer;
             }
         }
@@ -2233,32 +2270,59 @@ namespace Renci.SshNet
             return result;
         }
 
-        /// <summary>
-        /// Internals the download file.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="output">The output.</param>
-        /// <param name="asyncResult">An <see cref="IAsyncResult"/> that references the asynchronous request.</param>
-        /// <param name="downloadCallback">The download callback.</param>
-        /// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null"/>.</exception>
-        /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains whitespace.</exception>
-        /// <exception cref="SshConnectionException">Client not connected.</exception>
-        private void InternalDownloadFile(string path, Stream output, SftpDownloadAsyncResult? asyncResult, Action<ulong>? downloadCallback)
+#pragma warning disable S6966 // Awaitable method should be used
+        private async Task InternalDownloadFile(
+            string path,
+            Stream output,
+            SftpDownloadAsyncResult? asyncResult,
+            Action<ulong>? downloadCallback,
+            bool isAsync,
+            CancellationToken cancellationToken)
         {
-            ThrowHelper.ThrowIfNull(output);
-            ThrowHelper.ThrowIfNullOrWhiteSpace(path);
+            Debug.Assert(!string.IsNullOrWhiteSpace(path));
+            Debug.Assert(output is not null);
+            Debug.Assert(isAsync || cancellationToken == default);
 
             if (_sftpSession is null)
             {
                 throw new SshConnectionException("Client not connected.");
             }
 
-            var fullPath = _sftpSession.GetCanonicalPath(path);
+            SftpFileStream sftpStream;
 
-            using (var fileReader = ServiceFactory.CreateSftpFileReader(fullPath, _sftpSession, _bufferSize))
+            if (isAsync)
+            {
+                var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
+
+                sftpStream = await SftpFileStream.OpenAsync(
+                    _sftpSession,
+                    fullPath,
+                    FileMode.Open,
+                    FileAccess.Read,
+                    (int)_bufferSize,
+                    cancellationToken,
+                    isDownloadFile: true).ConfigureAwait(false);
+            }
+            else
             {
-                var totalBytesRead = 0UL;
+                var fullPath = _sftpSession.GetCanonicalPath(path);
 
+                sftpStream = SftpFileStream.Open(
+                    _sftpSession,
+                    fullPath,
+                    FileMode.Open,
+                    FileAccess.Read,
+                    (int)_bufferSize,
+                    isDownloadFile: true);
+            }
+
+            // The below is effectively sftpStream.CopyTo{Async}(output) with consideration
+            // for downloadCallback/asyncResult.
+
+            var buffer = ArrayPool<byte>.Shared.Rent(81920);
+            try
+            {
+                ulong totalBytesRead = 0;
                 while (true)
                 {
                     // Cancel download
@@ -2267,15 +2331,33 @@ namespace Renci.SshNet
                         break;
                     }
 
-                    var data = fileReader.Read();
-                    if (data.Length == 0)
+                    var bytesRead = isAsync
+#if NET
+                        ? await sftpStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)
+#else
+                        ? await sftpStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)
+#endif
+                        : sftpStream.Read(buffer, 0, buffer.Length);
+
+                    if (bytesRead == 0)
                     {
                         break;
                     }
 
-                    output.Write(data, 0, data.Length);
+                    if (isAsync)
+                    {
+#if NET
+                        await output.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
+#else
+                        await output.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
+#endif
+                    }
+                    else
+                    {
+                        output.Write(buffer, 0, bytesRead);
+                    }
 
-                    totalBytesRead += (ulong)data.Length;
+                    totalBytesRead += (ulong)bytesRead;
 
                     asyncResult?.Update(totalBytesRead);
 
@@ -2289,28 +2371,21 @@ namespace Renci.SshNet
                     }
                 }
             }
-        }
-
-        private async Task InternalDownloadFileAsync(string path, Stream output, CancellationToken cancellationToken)
-        {
-            ThrowHelper.ThrowIfNull(output);
-            ThrowHelper.ThrowIfNullOrWhiteSpace(path);
-
-            if (_sftpSession is null)
+            finally
             {
-                throw new SshConnectionException("Client not connected.");
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
+                ArrayPool<byte>.Shared.Return(buffer);
 
-            var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
-            var openStreamTask = SftpFileStream.OpenAsync(_sftpSession, fullPath, FileMode.Open, FileAccess.Read, (int)_bufferSize, cancellationToken);
-
-            using (var input = await openStreamTask.ConfigureAwait(false))
-            {
-                await input.CopyToAsync(output, 81920, cancellationToken).ConfigureAwait(false);
+                if (isAsync)
+                {
+                    await sftpStream.DisposeAsync().ConfigureAwait(false);
+                }
+                else
+                {
+                    sftpStream.Dispose();
+                }
             }
         }
+#pragma warning restore S6966 // Awaitable method should be used
 
 #pragma warning disable S6966 // Awaitable method should be used
         private async Task InternalUploadFile(

+ 1 - 1
test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs

@@ -65,7 +65,7 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
 
                 var cancelledToken = new CancellationToken(true);
 
-                await Assert.ThrowsExactlyAsync<OperationCanceledException>(() => sftp.DownloadFileAsync("/xxx/eee/yyy", Stream.Null, cancelledToken));
+                await Assert.ThrowsAsync<OperationCanceledException>(() => sftp.DownloadFileAsync("/xxx/eee/yyy", Stream.Null, cancelledToken));
             }
         }
 

+ 196 - 53
test/Renci.SshNet.IntegrationTests/SftpTests.cs

@@ -4353,11 +4353,11 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x04, fs.ReadByte());
 
                         var soughtOverReadBuffer = new byte[seekOffset - 1];
-                        Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length));
-                        Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer));
+                        fs.ReadExactly(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length);
+                        CollectionAssert.AreEqual(new byte[soughtOverReadBuffer.Length], soughtOverReadBuffer);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -4397,11 +4397,11 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x04, fs.ReadByte());
 
                         var soughtOverReadBuffer = new byte[seekOffset - 1];
-                        Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length));
-                        Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer));
+                        fs.ReadExactly(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length);
+                        CollectionAssert.AreEqual(new byte[soughtOverReadBuffer.Length], soughtOverReadBuffer);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -4438,7 +4438,7 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x00, fs.ReadByte());
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -4474,11 +4474,11 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x04, fs.ReadByte());
 
                         var soughtOverReadBuffer = new byte[550 - 1];
-                        Assert.AreEqual(550 - 1, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length));
-                        Assert.IsTrue(new byte[550 - 1].IsEqualTo(soughtOverReadBuffer));
+                        fs.ReadExactly(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length);
+                        CollectionAssert.AreEqual(new byte[550 - 1], soughtOverReadBuffer);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -4599,11 +4599,11 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x04, fs.ReadByte());
 
                         var soughtOverReadBuffer = new byte[seekOffset];
-                        Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length));
-                        Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer));
+                        fs.ReadExactly(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length);
+                        CollectionAssert.AreEqual(new byte[soughtOverReadBuffer.Length], soughtOverReadBuffer);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -4641,11 +4641,11 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x04, fs.ReadByte());
 
                         var soughtOverReadBuffer = new byte[seekOffset];
-                        Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length));
-                        Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer));
+                        fs.ReadExactly(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length);
+                        CollectionAssert.AreEqual(new byte[soughtOverReadBuffer.Length], soughtOverReadBuffer);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -4681,11 +4681,11 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x04, fs.ReadByte());
 
                         var soughtOverReadBuffer = new byte[seekOffset];
-                        Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length));
-                        Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer));
+                        fs.ReadExactly(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length);
+                        CollectionAssert.AreEqual(new byte[soughtOverReadBuffer.Length], soughtOverReadBuffer);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -4722,11 +4722,11 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x04, fs.ReadByte());
 
                         var soughtOverReadBuffer = new byte[seekOffset];
-                        Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length));
-                        Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer));
+                        fs.ReadExactly(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length);
+                        CollectionAssert.AreEqual(new byte[soughtOverReadBuffer.Length], soughtOverReadBuffer);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -4813,7 +4813,7 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(writeBuffer.Length, fs.Length);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -4844,8 +4844,8 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(writeBuffer.Length + 1, fs.Length);
 
                         var readBuffer = new byte[writeBuffer.Length - 3];
-                        Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
-                        Assert.IsTrue(readBuffer.SequenceEqual(writeBuffer.Take(readBuffer.Length)));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
+                        CollectionAssert.AreEqual(writeBuffer.Take(readBuffer.Length), readBuffer);
 
                         Assert.AreEqual(0x01, fs.ReadByte());
                         Assert.AreEqual(0x05, fs.ReadByte());
@@ -4884,8 +4884,8 @@ namespace Renci.SshNet.IntegrationTests
 
                         // First part of file should not have been touched
                         var readBuffer = new byte[(int)client.BufferSize * 2];
-                        Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
-                        Assert.IsTrue(readBuffer.SequenceEqual(writeBuffer.Take(readBuffer.Length)));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
+                        CollectionAssert.AreEqual(writeBuffer.Take(readBuffer.Length), readBuffer);
 
                         // Check part that should have been updated
                         Assert.AreEqual(0x01, fs.ReadByte());
@@ -4895,8 +4895,10 @@ namespace Renci.SshNet.IntegrationTests
 
                         // Remaining bytes should not have been touched
                         readBuffer = new byte[((int)client.BufferSize * 2) - 4];
-                        Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
-                        Assert.IsTrue(readBuffer.SequenceEqual(writeBuffer.Skip(((int)client.BufferSize * 2) + 4).Take(readBuffer.Length)));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
+                        CollectionAssert.AreEqual(
+                            writeBuffer.Skip(((int)client.BufferSize * 2) + 4).Take(readBuffer.Length).ToArray(),
+                            readBuffer);
 
                         // Ensure we've reached end of the stream
                         Assert.AreEqual(-1, fs.ReadByte());
@@ -4987,7 +4989,7 @@ namespace Renci.SshNet.IntegrationTests
                     {
                         var readBuffer = new byte[200];
 
-                        Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
 
                         var newPosition = fs.Seek(offset: 3L, SeekOrigin.Begin);
 
@@ -5062,11 +5064,11 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x04, fs.ReadByte());
 
                         var soughtOverReadBuffer = new byte[seekOffset - 1];
-                        Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length));
-                        Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer));
+                        fs.ReadExactly(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length);
+                        CollectionAssert.AreEqual(new byte[soughtOverReadBuffer.Length], soughtOverReadBuffer);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -5104,11 +5106,11 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x04, fs.ReadByte());
 
                         var soughtOverReadBuffer = new byte[seekOffset - 1];
-                        Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length));
-                        Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer));
+                        fs.ReadExactly(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length);
+                        CollectionAssert.AreEqual(new byte[soughtOverReadBuffer.Length], soughtOverReadBuffer);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -5148,7 +5150,7 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x00, fs.ReadByte());
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -5187,11 +5189,11 @@ namespace Renci.SshNet.IntegrationTests
                         Assert.AreEqual(0x04, fs.ReadByte());
 
                         var soughtOverReadBuffer = new byte[seekOffset - 1];
-                        Assert.AreEqual(seekOffset - 1, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length));
-                        Assert.IsTrue(new byte[seekOffset - 1].IsEqualTo(soughtOverReadBuffer));
+                        fs.ReadExactly(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length);
+                        CollectionAssert.AreEqual(new byte[seekOffset - 1], soughtOverReadBuffer);
 
                         var readBuffer = new byte[writeBuffer.Length];
-                        Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length));
+                        fs.ReadExactly(readBuffer, offset: 0, readBuffer.Length);
                         CollectionAssert.AreEqual(writeBuffer, readBuffer);
 
                         // Ensure we've reached end of the stream
@@ -6203,6 +6205,136 @@ namespace Renci.SshNet.IntegrationTests
             }
         }
 
+        [TestMethod]
+        public void Sftp_SftpFileStream_Fuzz()
+        {
+            const int OperationCount = 100;
+            const int MaxBufferSize = 1000;
+            const int MaxFileSize = 15_000;
+
+            int seed = Environment.TickCount;
+
+            Console.WriteLine("Using seed " + seed);
+
+            var random = new Random(seed);
+
+            using var client = new SftpClient(_connectionInfoFactory.Create())
+            {
+                BufferSize = 100
+            };
+            client.Connect();
+
+            // We will perform operations on an SftpFileStream and a local
+            // System.IO.FileStream, and check that the results are the same.
+            // This could use a MemoryStream for the local side, except for the
+            // fact that performing a 0-byte write at a position beyond the length
+            // of the MemoryStream causes its length to increase, which is not the
+            // case for FileStream. Since we've got 'FileStream' in the name, we
+            // check that we align with FileStream's behaviour.
+
+            string remoteFilePath = GenerateUniqueRemoteFileName();
+            string localFilePath = Path.GetTempFileName();
+
+            byte[] fileBytes = new byte[1024];
+            random.NextBytes(fileBytes);
+
+            File.WriteAllBytes(localFilePath, fileBytes);
+            client.WriteAllBytes(remoteFilePath, fileBytes);
+
+            try
+            {
+                using (var local = File.Open(localFilePath, FileMode.Open, FileAccess.ReadWrite))
+                using (var remote = client.Open(remoteFilePath, FileMode.Open, FileAccess.ReadWrite))
+                {
+                    for (int i = 0; i < OperationCount; i++)
+                    {
+#pragma warning disable IDE0010 // Add missing cases
+                        int op = random.Next(5);
+                        switch (op)
+                        {
+                            case 0 when local.Length < MaxFileSize: // Write
+                                {
+                                    var buffer = new byte[random.Next(0, MaxBufferSize)];
+                                    random.NextBytes(buffer);
+                                    int offset = random.Next(0, buffer.Length + 1);
+                                    int count = random.Next(0, buffer.Length - offset + 1);
+
+                                    remote.Write(buffer, offset, count);
+                                    local.Write(buffer, offset, count);
+                                    break;
+                                }
+                            case 1: // Read
+                                {
+                                    var remoteBuffer = new byte[random.Next(0, MaxBufferSize)];
+                                    var localBuffer = new byte[remoteBuffer.Length];
+                                    int offset = random.Next(0, remoteBuffer.Length + 1);
+                                    int count = random.Next(0, remoteBuffer.Length - offset + 1);
+
+                                    int remoteRead = ReadExactly(remote, remoteBuffer, offset, count);
+                                    int localRead = ReadExactly(local, localBuffer, offset, count);
+
+                                    Assert.AreEqual(localRead, remoteRead);
+                                    CollectionAssert.AreEqual(localBuffer, remoteBuffer);
+                                    break;
+                                }
+                            case 2 when local.Length < MaxFileSize: // Seek
+                                {
+                                    int position = (int)local.Position;
+                                    int length = (int)local.Length;
+
+                                    SeekOrigin origin = (SeekOrigin)random.Next(0, 3);
+                                    long offset = 0;
+                                    switch (origin)
+                                    {
+                                        case SeekOrigin.Begin:
+                                            offset = random.Next(0, length * 2);
+                                            break;
+                                        case SeekOrigin.Current:
+                                            offset = random.Next(-position, position);
+                                            break;
+                                        case SeekOrigin.End:
+                                            offset = random.Next(-length, length);
+                                            break;
+                                    }
+                                    long newPosRemote = remote.Seek(offset, origin);
+                                    long newPosLocal = local.Seek(offset, origin);
+                                    Assert.AreEqual(newPosLocal, newPosRemote);
+                                    Assert.AreEqual(local.Length, remote.Length);
+                                    break;
+                                }
+                            case 3: // SetLength
+                                {
+                                    long newLength = random.Next(0, MaxFileSize);
+                                    remote.SetLength(newLength);
+                                    local.SetLength(newLength);
+                                    Assert.AreEqual(local.Length, remote.Length);
+                                    Assert.AreEqual(local.Position, remote.Position);
+                                    break;
+                                }
+                            case 4: // Flush
+                                {
+                                    remote.Flush();
+                                    local.Flush();
+                                    break;
+                                }
+                        }
+#pragma warning restore IDE0010 // Add missing cases
+                    }
+                }
+
+                CollectionAssert.AreEqual(File.ReadAllBytes(localFilePath), client.ReadAllBytes(remoteFilePath));
+            }
+            finally
+            {
+                File.Delete(localFilePath);
+
+                if (client.Exists(remoteFilePath))
+                {
+                    client.DeleteFile(remoteFilePath);
+                }
+            }
+        }
+
         private static IEnumerable<object[]> GetSftpUploadFileFileStreamData()
         {
             yield return new object[] { 0 };
@@ -6228,7 +6360,7 @@ namespace Renci.SshNet.IntegrationTests
         {
             Console.Write($"Downloading '{path}'");
 
-            var random = new Random().Next(1, 6);
+            var random = new Random().Next(1, 7);
             switch (random)
             {
                 case 1:
@@ -6258,8 +6390,7 @@ namespace Renci.SshNet.IntegrationTests
                     }
 
                     break;
-                default:
-                    Debug.Assert(random == 5);
+                case 5:
                     Console.WriteLine($" with {nameof(SftpFileStream.CopyToAsync)}");
 
                     using (var fs = client.OpenAsync(path, FileMode.Open, FileAccess.Read, CancellationToken.None).GetAwaiter().GetResult())
@@ -6267,6 +6398,15 @@ namespace Renci.SshNet.IntegrationTests
                         fs.CopyToAsync(output).GetAwaiter().GetResult();
                     }
 
+                    break;
+                default:
+                    Debug.Assert(random == 6);
+                    Console.WriteLine($" with {nameof(SftpClient.ReadAllBytes)}");
+
+                    byte[] bytes = client.ReadAllBytes(path);
+
+                    output.Write(bytes, 0, bytes.Length);
+
                     break;
             }
         }
@@ -6292,21 +6432,24 @@ namespace Renci.SshNet.IntegrationTests
             return (length / 1024m) / (elapsedMilliseconds / 1000m);
         }
 
-        private static void SftpCreateRemoteFile(SftpClient client, string remoteFile, int size)
+        /// <summary>
+        /// Similar to the netcore ReadExactly but without throwing on end of stream.
+        /// </summary>
+        private static int ReadExactly(Stream stream, byte[] buffer, int offset, int count)
         {
-            var file = CreateTempFile(size);
-
-            try
+            int totalRead = 0;
+            while (totalRead < count)
             {
-                using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
+                int read = stream.Read(buffer, offset + totalRead, count - totalRead);
+                if (read == 0)
                 {
-                    client.UploadFile(fs, remoteFile);
+                    return totalRead;
                 }
+
+                totalRead += read;
             }
-            finally
-            {
-                File.Delete(file);
-            }
+
+            return totalRead;
         }
 
         private static byte[] GenerateRandom(int size)

+ 0 - 99
test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs

@@ -1,99 +0,0 @@
-using System;
-
-using Microsoft.Extensions.Logging.Abstractions;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-namespace Renci.SshNet.Tests.Classes
-{
-    [TestClass]
-    public class ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException
-    {
-        private ServiceFactory _serviceFactory;
-        private Mock<ISftpSession> _sftpSessionMock;
-        private Mock<ISftpFileReader> _sftpFileReaderMock;
-        private uint _bufferSize;
-        private string _fileName;
-        private SftpOpenAsyncResult _openAsyncResult;
-        private byte[] _handle;
-        private SFtpStatAsyncResult _statAsyncResult;
-        private uint _chunkSize;
-        private ISftpFileReader _actual;
-
-        private void SetupData()
-        {
-            var random = new Random();
-
-            _bufferSize = (uint)random.Next(1, int.MaxValue);
-            _openAsyncResult = new SftpOpenAsyncResult(null, null);
-            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
-            _statAsyncResult = new SFtpStatAsyncResult(null, null);
-            _fileName = random.Next().ToString();
-            _chunkSize = (uint)random.Next(1, int.MaxValue);
-        }
-
-        private void CreateMocks()
-        {
-            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
-            _sftpSessionMock.Setup(p => p.SessionLoggerFactory).Returns(NullLoggerFactory.Instance);
-            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
-        }
-
-        private void SetupMocks()
-        {
-            var seq = new MockSequence();
-
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
-                            .Returns(_openAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndOpen(_openAsyncResult))
-                            .Returns(_handle);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginLStat(_fileName, null, null))
-                            .Returns(_statAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
-                            .Returns(_chunkSize);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndLStat(_statAsyncResult))
-                            .Throws(new SshException());
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 10, null))
-                            .Returns(_sftpFileReaderMock.Object);
-        }
-
-        private void Arrange()
-        {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
-
-            _serviceFactory = new ServiceFactory();
-        }
-
-        [TestInitialize]
-        public void Initialize()
-        {
-            Arrange();
-            Act();
-        }
-
-        private void Act()
-        {
-            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
-        }
-
-        [TestMethod]
-        public void CreateSftpFileReaderShouldReturnCreatedInstance()
-        {
-            Assert.IsNotNull(_actual);
-            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
-        }
-    }
-}

+ 0 - 101
test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs

@@ -1,101 +0,0 @@
-using System;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Sftp;
-using Renci.SshNet.Tests.Common;
-
-namespace Renci.SshNet.Tests.Classes
-{
-    [TestClass]
-    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize
-    {
-        private ServiceFactory _serviceFactory;
-        private Mock<ISftpSession> _sftpSessionMock;
-        private Mock<ISftpFileReader> _sftpFileReaderMock;
-        private uint _bufferSize;
-        private string _fileName;
-        private SftpOpenAsyncResult _openAsyncResult;
-        private byte[] _handle;
-        private SFtpStatAsyncResult _statAsyncResult;
-        private uint _chunkSize;
-        private SftpFileAttributes _fileAttributes;
-        private long _fileSize;
-        private ISftpFileReader _actual;
-
-        private void SetupData()
-        {
-            var random = new Random();
-
-            _bufferSize = (uint)random.Next(1, int.MaxValue);
-            _openAsyncResult = new SftpOpenAsyncResult(null, null);
-            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
-            _statAsyncResult = new SFtpStatAsyncResult(null, null);
-            _fileName = random.Next().ToString();
-            _chunkSize = (uint)random.Next(1000, 5000);
-            _fileSize = (_chunkSize * 6) - 10;
-            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
-        }
-
-        private void CreateMocks()
-        {
-            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
-            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
-        }
-
-        private void SetupMocks()
-        {
-            var seq = new MockSequence();
-
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
-                            .Returns(_openAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndOpen(_openAsyncResult))
-                            .Returns(_handle);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginLStat(_fileName, null, null))
-                            .Returns(_statAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
-                            .Returns(_chunkSize);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndLStat(_statAsyncResult))
-                            .Returns(_fileAttributes);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 7, _fileSize))
-                            .Returns(_sftpFileReaderMock.Object);
-        }
-
-        private void Arrange()
-        {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
-
-            _serviceFactory = new ServiceFactory();
-        }
-
-        [TestInitialize]
-        public void Initialize()
-        {
-            Arrange();
-            Act();
-        }
-
-        private void Act()
-        {
-            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
-        }
-
-        [TestMethod]
-        public void CreateSftpFileReaderShouldReturnCreatedInstance()
-        {
-            Assert.IsNotNull(_actual);
-            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
-        }
-    }
-}

+ 0 - 101
test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs

@@ -1,101 +0,0 @@
-using System;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Sftp;
-using Renci.SshNet.Tests.Common;
-
-namespace Renci.SshNet.Tests.Classes
-{
-    [TestClass]
-    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize
-    {
-        private ServiceFactory _serviceFactory;
-        private Mock<ISftpSession> _sftpSessionMock;
-        private Mock<ISftpFileReader> _sftpFileReaderMock;
-        private uint _bufferSize;
-        private string _fileName;
-        private SftpOpenAsyncResult _openAsyncResult;
-        private byte[] _handle;
-        private SFtpStatAsyncResult _statAsyncResult;
-        private uint _chunkSize;
-        private SftpFileAttributes _fileAttributes;
-        private long _fileSize;
-        private ISftpFileReader _actual;
-
-        private void SetupData()
-        {
-            var random = new Random();
-
-            _bufferSize = (uint)random.Next(1, int.MaxValue);
-            _openAsyncResult = new SftpOpenAsyncResult(null, null);
-            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
-            _statAsyncResult = new SFtpStatAsyncResult(null, null);
-            _fileName = random.Next().ToString();
-            _chunkSize = (uint)random.Next(1000, int.MaxValue);
-            _fileSize = _chunkSize;
-            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
-        }
-
-        private void CreateMocks()
-        {
-            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
-            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
-        }
-
-        private void SetupMocks()
-        {
-            var seq = new MockSequence();
-
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
-                            .Returns(_openAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndOpen(_openAsyncResult))
-                            .Returns(_handle);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginLStat(_fileName, null, null))
-                            .Returns(_statAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
-                            .Returns(_chunkSize);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndLStat(_statAsyncResult))
-                            .Returns(_fileAttributes);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 2, _fileSize))
-                            .Returns(_sftpFileReaderMock.Object);
-        }
-
-        private void Arrange()
-        {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
-
-            _serviceFactory = new ServiceFactory();
-        }
-
-        [TestInitialize]
-        public void Initialize()
-        {
-            Arrange();
-            Act();
-        }
-
-        private void Act()
-        {
-            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
-        }
-
-        [TestMethod]
-        public void CreateSftpFileReaderShouldReturnCreatedInstance()
-        {
-            Assert.IsNotNull(_actual);
-            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
-        }
-    }
-}

+ 0 - 101
test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize.cs

@@ -1,101 +0,0 @@
-using System;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Sftp;
-using Renci.SshNet.Tests.Common;
-
-namespace Renci.SshNet.Tests.Classes
-{
-    [TestClass]
-    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsExactlyFiveTimesGreaterThanChunkSize
-    {
-        private ServiceFactory _serviceFactory;
-        private Mock<ISftpSession> _sftpSessionMock;
-        private Mock<ISftpFileReader> _sftpFileReaderMock;
-        private uint _bufferSize;
-        private string _fileName;
-        private SftpOpenAsyncResult _openAsyncResult;
-        private byte[] _handle;
-        private SFtpStatAsyncResult _statAsyncResult;
-        private uint _chunkSize;
-        private SftpFileAttributes _fileAttributes;
-        private long _fileSize;
-        private ISftpFileReader _actual;
-
-        private void SetupData()
-        {
-            var random = new Random();
-
-            _bufferSize = (uint)random.Next(1, int.MaxValue);
-            _openAsyncResult = new SftpOpenAsyncResult(null, null);
-            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
-            _statAsyncResult = new SFtpStatAsyncResult(null, null);
-            _fileName = random.Next().ToString();
-            _chunkSize = (uint)random.Next(1000, 5000);
-            _fileSize = _chunkSize * 5;
-            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
-        }
-
-        private void CreateMocks()
-        {
-            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
-            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
-        }
-
-        private void SetupMocks()
-        {
-            var seq = new MockSequence();
-
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
-                            .Returns(_openAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndOpen(_openAsyncResult))
-                            .Returns(_handle);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginLStat(_fileName, null, null))
-                            .Returns(_statAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
-                            .Returns(_chunkSize);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndLStat(_statAsyncResult))
-                            .Returns(_fileAttributes);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 6, _fileSize))
-                            .Returns(_sftpFileReaderMock.Object);
-        }
-
-        private void Arrange()
-        {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
-
-            _serviceFactory = new ServiceFactory();
-        }
-
-        [TestInitialize]
-        public void Initialize()
-        {
-            Arrange();
-            Act();
-        }
-
-        private void Act()
-        {
-            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
-        }
-
-        [TestMethod]
-        public void CreateSftpFileReaderShouldReturnCreatedInstance()
-        {
-            Assert.IsNotNull(_actual);
-            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
-        }
-    }
-}

+ 0 - 101
test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize.cs

@@ -1,101 +0,0 @@
-using System;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Sftp;
-using Renci.SshNet.Tests.Common;
-
-namespace Renci.SshNet.Tests.Classes
-{
-    [TestClass]
-    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLessThanChunkSize
-    {
-        private ServiceFactory _serviceFactory;
-        private Mock<ISftpSession> _sftpSessionMock;
-        private Mock<ISftpFileReader> _sftpFileReaderMock;
-        private uint _bufferSize;
-        private string _fileName;
-        private SftpOpenAsyncResult _openAsyncResult;
-        private byte[] _handle;
-        private SFtpStatAsyncResult _statAsyncResult;
-        private uint _chunkSize;
-        private SftpFileAttributes _fileAttributes;
-        private long _fileSize;
-        private ISftpFileReader _actual;
-
-        private void SetupData()
-        {
-            var random = new Random();
-
-            _bufferSize = (uint)random.Next(1, int.MaxValue);
-            _openAsyncResult = new SftpOpenAsyncResult(null, null);
-            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
-            _statAsyncResult = new SFtpStatAsyncResult(null, null);
-            _fileName = random.Next().ToString();
-            _chunkSize = (uint)random.Next(1000, int.MaxValue);
-            _fileSize = _chunkSize - random.Next(1, 10);
-            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
-        }
-
-        private void CreateMocks()
-        {
-            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
-            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
-        }
-
-        private void SetupMocks()
-        {
-            var seq = new MockSequence();
-
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
-                            .Returns(_openAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndOpen(_openAsyncResult))
-                            .Returns(_handle);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginLStat(_fileName, null, null))
-                            .Returns(_statAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
-                            .Returns(_chunkSize);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndLStat(_statAsyncResult))
-                            .Returns(_fileAttributes);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 2, _fileSize))
-                            .Returns(_sftpFileReaderMock.Object);
-        }
-
-        private void Arrange()
-        {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
-
-            _serviceFactory = new ServiceFactory();
-        }
-
-        [TestInitialize]
-        public void Initialize()
-        {
-            Arrange();
-            Act();
-        }
-
-        private void Act()
-        {
-            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
-        }
-
-        [TestMethod]
-        public void CreateSftpFileReaderShouldReturnCreatedInstance()
-        {
-            Assert.IsNotNull(_actual);
-            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
-        }
-    }
-}

+ 0 - 101
test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize.cs

@@ -1,101 +0,0 @@
-using System;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Sftp;
-using Renci.SshNet.Tests.Common;
-
-namespace Renci.SshNet.Tests.Classes
-{
-    [TestClass]
-    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsLittleMoreThanFiveTimesGreaterThanChunkSize
-    {
-        private ServiceFactory _serviceFactory;
-        private Mock<ISftpSession> _sftpSessionMock;
-        private Mock<ISftpFileReader> _sftpFileReaderMock;
-        private uint _bufferSize;
-        private string _fileName;
-        private SftpOpenAsyncResult _openAsyncResult;
-        private byte[] _handle;
-        private SFtpStatAsyncResult _statAsyncResult;
-        private uint _chunkSize;
-        private SftpFileAttributes _fileAttributes;
-        private long _fileSize;
-        private ISftpFileReader _actual;
-
-        private void SetupData()
-        {
-            var random = new Random();
-
-            _bufferSize = (uint)random.Next(1, int.MaxValue);
-            _openAsyncResult = new SftpOpenAsyncResult(null, null);
-            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
-            _statAsyncResult = new SFtpStatAsyncResult(null, null);
-            _fileName = random.Next().ToString();
-            _chunkSize = (uint)random.Next(1000, 5000);
-            _fileSize = (_chunkSize * 5) + 10;
-            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
-        }
-
-        private void CreateMocks()
-        {
-            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
-            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
-        }
-
-        private void SetupMocks()
-        {
-            var seq = new MockSequence();
-
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
-                            .Returns(_openAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndOpen(_openAsyncResult))
-                            .Returns(_handle);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginLStat(_fileName, null, null))
-                            .Returns(_statAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
-                            .Returns(_chunkSize);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndLStat(_statAsyncResult))
-                            .Returns(_fileAttributes);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 7, _fileSize))
-                            .Returns(_sftpFileReaderMock.Object);
-        }
-
-        private void Arrange()
-        {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
-
-            _serviceFactory = new ServiceFactory();
-        }
-
-        [TestInitialize]
-        public void Initialize()
-        {
-            Arrange();
-            Act();
-        }
-
-        private void Act()
-        {
-            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
-        }
-
-        [TestMethod]
-        public void CreateSftpFileReaderShouldReturnCreatedInstance()
-        {
-            Assert.IsNotNull(_actual);
-            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
-        }
-    }
-}

+ 0 - 103
test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanMaxPendingReadsTimesChunkSize.cs

@@ -1,103 +0,0 @@
-using System;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Sftp;
-using Renci.SshNet.Tests.Common;
-
-namespace Renci.SshNet.Tests.Classes
-{
-    [TestClass]
-    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsMoreThanMaxPendingReadsTimesChunkSize
-    {
-        private ServiceFactory _serviceFactory;
-        private Mock<ISftpSession> _sftpSessionMock;
-        private Mock<ISftpFileReader> _sftpFileReaderMock;
-        private uint _bufferSize;
-        private string _fileName;
-        private SftpOpenAsyncResult _openAsyncResult;
-        private byte[] _handle;
-        private SFtpStatAsyncResult _statAsyncResult;
-        private uint _chunkSize;
-        private SftpFileAttributes _fileAttributes;
-        private long _fileSize;
-        private ISftpFileReader _actual;
-        private int _maxPendingReads;
-
-        private void SetupData()
-        {
-            var random = new Random();
-
-            _maxPendingReads = 100;
-            _bufferSize = (uint)random.Next(1, int.MaxValue);
-            _openAsyncResult = new SftpOpenAsyncResult(null, null);
-            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
-            _statAsyncResult = new SFtpStatAsyncResult(null, null);
-            _fileName = random.Next().ToString();
-            _chunkSize = (uint)random.Next(1000, 5000);
-            _fileSize = _chunkSize * random.Next(_maxPendingReads + 1, _maxPendingReads * 2);
-            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
-        }
-
-        private void CreateMocks()
-        {
-            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
-            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
-        }
-
-        private void SetupMocks()
-        {
-            var seq = new MockSequence();
-
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
-                            .Returns(_openAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndOpen(_openAsyncResult))
-                            .Returns(_handle);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginLStat(_fileName, null, null))
-                            .Returns(_statAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
-                            .Returns(_chunkSize);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndLStat(_statAsyncResult))
-                            .Returns(_fileAttributes);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, _maxPendingReads, _fileSize))
-                            .Returns(_sftpFileReaderMock.Object);
-        }
-
-        private void Arrange()
-        {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
-
-            _serviceFactory = new ServiceFactory();
-        }
-
-        [TestInitialize]
-        public void Initialize()
-        {
-            Arrange();
-            Act();
-        }
-
-        private void Act()
-        {
-            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
-        }
-
-        [TestMethod]
-        public void CreateSftpFileReaderShouldReturnCreatedInstance()
-        {
-            Assert.IsNotNull(_actual);
-            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
-        }
-    }
-}

+ 0 - 101
test/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero.cs

@@ -1,101 +0,0 @@
-using System;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Sftp;
-using Renci.SshNet.Tests.Common;
-
-namespace Renci.SshNet.Tests.Classes
-{
-    [TestClass]
-    public class ServiceFactoryTest_CreateSftpFileReader_FileSizeIsZero
-    {
-        private ServiceFactory _serviceFactory;
-        private Mock<ISftpSession> _sftpSessionMock;
-        private Mock<ISftpFileReader> _sftpFileReaderMock;
-        private uint _bufferSize;
-        private string _fileName;
-        private SftpOpenAsyncResult _openAsyncResult;
-        private byte[] _handle;
-        private SFtpStatAsyncResult _statAsyncResult;
-        private uint _chunkSize;
-        private long _fileSize;
-        private SftpFileAttributes _fileAttributes;
-        private ISftpFileReader _actual;
-
-        private void SetupData()
-        {
-            var random = new Random();
-
-            _bufferSize = (uint)random.Next(1, int.MaxValue);
-            _openAsyncResult = new SftpOpenAsyncResult(null, null);
-            _handle = CryptoAbstraction.GenerateRandom(random.Next(1, 10));
-            _statAsyncResult = new SFtpStatAsyncResult(null, null);
-            _fileName = random.Next().ToString();
-            _chunkSize = (uint)random.Next(1, int.MaxValue);
-            _fileSize = 0L;
-            _fileAttributes = new SftpFileAttributesBuilder().WithSize(_fileSize).Build();
-        }
-
-        private void CreateMocks()
-        {
-            _sftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
-            _sftpFileReaderMock = new Mock<ISftpFileReader>(MockBehavior.Strict);
-        }
-
-        private void SetupMocks()
-        {
-            var seq = new MockSequence();
-
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginOpen(_fileName, Flags.Read, null, null))
-                            .Returns(_openAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndOpen(_openAsyncResult))
-                            .Returns(_handle);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.BeginLStat(_fileName, null, null))
-                            .Returns(_statAsyncResult);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
-                            .Returns(_chunkSize);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.EndLStat(_statAsyncResult))
-                            .Returns(_fileAttributes);
-            _sftpSessionMock.InSequence(seq)
-                            .Setup(p => p.CreateFileReader(_handle, _sftpSessionMock.Object, _chunkSize, 1, _fileSize))
-                            .Returns(_sftpFileReaderMock.Object);
-        }
-
-        private void Arrange()
-        {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
-
-            _serviceFactory = new ServiceFactory();
-        }
-
-        [TestInitialize]
-        public void Initialize()
-        {
-            Arrange();
-            Act();
-        }
-
-        private void Act()
-        {
-            _actual = _serviceFactory.CreateSftpFileReader(_fileName, _sftpSessionMock.Object, _bufferSize);
-        }
-
-        [TestMethod]
-        public void CreateSftpFileReaderShouldReturnCreatedInstance()
-        {
-            Assert.IsNotNull(_actual);
-            Assert.AreSame(_sftpFileReaderMock.Object, _actual);
-        }
-    }
-}

+ 0 - 63
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTestBase.cs

@@ -1,63 +0,0 @@
-using System;
-using System.Threading;
-
-using Microsoft.Extensions.Logging.Abstractions;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    public abstract class SftpFileReaderTestBase
-    {
-        internal Mock<ISftpSession> SftpSessionMock { get; private set; }
-
-        protected abstract void SetupData();
-
-        protected void CreateMocks()
-        {
-            SftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
-            SftpSessionMock.Setup(s => s.SessionLoggerFactory).Returns(NullLoggerFactory.Instance);
-        }
-
-        protected abstract void SetupMocks();
-
-        protected virtual void Arrange()
-        {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
-        }
-
-        [TestInitialize]
-        public void SetUp()
-        {
-            Arrange();
-            Act();
-        }
-
-        protected abstract void Act();
-
-        protected static byte[] CreateByteArray(Random random, int length)
-        {
-            var chunk = new byte[length];
-            random.NextBytes(chunk);
-            return chunk;
-        }
-
-        protected static int WaitAny(WaitHandle[] waitHandles, int millisecondsTimeout)
-        {
-            var result = WaitHandle.WaitAny(waitHandles, millisecondsTimeout);
-
-            if (result == WaitHandle.WaitTimeout)
-            {
-                throw new SshOperationTimeoutException();
-            }
-
-            return result;
-        }
-    }
-}

+ 0 - 173
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead.cs

@@ -1,173 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_DisposeShouldUnblockReadAndReadAhead : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private SftpFileReader _reader;
-        private ObjectDisposedException _actualException;
-        private AsyncCallback _readAsyncCallback;
-        private EventWaitHandle _disposeCompleted;
-
-        [TestCleanup]
-        public void TearDown()
-        {
-            _disposeCompleted?.Dispose();
-        }
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _fileSize = 5000;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-            _disposeCompleted = new ManualResetEvent(false);
-            _readAsyncCallback = null;
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                               .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                                    {
-                                        _waitHandleArray[0] = disposingWaitHandle;
-                                        _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                                        return _waitHandleArray;
-                                    });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Returns<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                    {
-                                        _readAsyncCallback = callback;
-                                        return null;
-                                    });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.IsOpen)
-                               .Returns(true);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginClose(_handle, null, null))
-                               .Returns(_closeAsyncResult);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.EndClose(_closeAsyncResult));
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            ThreadAbstraction.ExecuteThread(() =>
-                {
-                    Thread.Sleep(500);
-                    _reader.Dispose();
-                    _ = _disposeCompleted.Set();
-                });
-
-            try
-            {
-                _ = _reader.Read();
-                Assert.Fail();
-            }
-            catch (ObjectDisposedException ex)
-            {
-                _actualException = ex;
-            }
-
-            // Dispose may unblock Read() before the dispose has fully completed, so
-            // let's wait until it has completed
-            _ = _disposeCompleted.WaitOne(500);
-        }
-
-        [TestMethod]
-        public void ReadShouldHaveThrownObjectDisposedException()
-        {
-            Assert.IsNotNull(_actualException);
-            Assert.AreEqual(typeof(SftpFileReader).FullName, _actualException.ObjectName);
-        }
-
-        [TestMethod]
-        public void ReadAfterDisposeShouldThrowObjectDisposedException()
-        {
-            try
-            {
-                _ = _reader.Read();
-                Assert.Fail();
-            }
-            catch (ObjectDisposedException ex)
-            {
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual(typeof(SftpFileReader).FullName, ex.ObjectName);
-            }
-        }
-
-        [TestMethod]
-        public void HandleShouldHaveBeenClosed()
-        {
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-
-        [TestMethod]
-        public void DisposeShouldCompleteImmediatelyAndNotAttemptToCloseHandleAgain()
-        {
-            var stopwatch = Stopwatch.StartNew();
-            _reader.Dispose();
-            stopwatch.Stop();
-
-            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
-
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-
-        [TestMethod]
-        public void InvokeOfReadAheadCallbackShouldCompleteImmediately()
-        {
-            Assert.IsNotNull(_readAsyncCallback);
-
-            _readAsyncCallback(new SftpReadAsyncResult(null, null));
-        }
-    }
-}

+ 0 - 132
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsNotOpen.cs

@@ -1,132 +0,0 @@
-using System;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_Dispose_SftpSessionIsNotOpen : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpFileReader _reader;
-        private AsyncCallback _readAsyncCallback;
-        private ManualResetEvent _beginReadInvoked;
-        private EventWaitHandle _disposeCompleted;
-
-        [TestCleanup]
-        public void TearDown()
-        {
-            _beginReadInvoked?.Dispose();
-            _disposeCompleted?.Dispose();
-        }
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _fileSize = 5000;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _beginReadInvoked = new ManualResetEvent(false);
-            _disposeCompleted = new ManualResetEvent(false);
-            _readAsyncCallback = null;
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                               .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                                    {
-                                        _waitHandleArray[0] = disposingWaitHandle;
-                                        _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                                        return _waitHandleArray;
-                                    });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback(() =>
-                                    {
-                                        // harden test by making sure that we've invoked BeginRead before Dispose is invoked
-                                        _ = _beginReadInvoked.Set();
-                                    })
-                               .Returns<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                    {
-                                        _readAsyncCallback = callback;
-                                        return null;
-                                    })
-                               .Callback(() =>
-                                    {
-                                        // wait until Dispose has been invoked on reader to allow us to harden test, and
-                                        // verify whether Dispose will prevent us from entering the read-ahead loop again
-                                        _ = _waitHandleArray[0].WaitOne();
-                                    });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.IsOpen)
-                               .Returns(false);
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            Assert.IsTrue(_beginReadInvoked.WaitOne(5000));
-            _reader.Dispose();
-        }
-
-        [TestMethod]
-        public void ReadAfterDisposeShouldThrowObjectDisposedException()
-        {
-            try
-            {
-                _ = _reader.Read();
-                Assert.Fail();
-            }
-            catch (ObjectDisposedException ex)
-            {
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual(typeof(SftpFileReader).FullName, ex.ObjectName);
-            }
-        }
-
-        [TestMethod]
-        public void InvokeOfReadAheadCallbackShouldCompleteImmediately()
-        {
-            Assert.IsNotNull(_readAsyncCallback);
-
-            _readAsyncCallback(new SftpReadAsyncResult(null, null));
-        }
-
-        [TestMethod]
-        public void BeginCloseOnSftpSessionShouldNeverHaveBeenInvoked()
-        {
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Never);
-        }
-    }
-}

+ 0 - 136
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsOpen_BeginCloseThrowsException.cs

@@ -1,136 +0,0 @@
-using System;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_Dispose_SftpSessionIsOpen_BeginCloseThrowsException : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpFileReader _reader;
-        private AsyncCallback _readAsyncCallback;
-        private ManualResetEvent _beginReadInvoked;
-        private EventWaitHandle _disposeCompleted;
-
-        [TestCleanup]
-        public void TearDown()
-        {
-            _beginReadInvoked?.Dispose();
-            _disposeCompleted?.Dispose();
-        }
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _fileSize = 5000;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _beginReadInvoked = new ManualResetEvent(false);
-            _disposeCompleted = new ManualResetEvent(false);
-            _readAsyncCallback = null;
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                               .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                                    {
-                                        _waitHandleArray[0] = disposingWaitHandle;
-                                        _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                                        return _waitHandleArray;
-                                    });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback(() =>
-                                    {
-                                        // harden test by making sure that we've invoked BeginRead before Dispose is invoked
-                                        _ = _beginReadInvoked.Set();
-                                    })
-                               .Returns<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                    {
-                                        _readAsyncCallback = callback;
-                                        return null;
-                                    })
-                               .Callback(() =>
-                                    {
-                                        // wait until Dispose has been invoked on reader to allow us to harden test, and
-                                        // verify whether Dispose will prevent us from entering the read-ahead loop again
-                                        _ = _waitHandleArray[0].WaitOne();
-                                    });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.IsOpen)
-                               .Returns(true);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginClose(_handle, null, null))
-                               .Throws(new SshException());
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            Assert.IsTrue(_beginReadInvoked.WaitOne(5000));
-            _reader.Dispose();
-        }
-
-        [TestMethod]
-        public void ReadAfterDisposeShouldThrowObjectDisposedException()
-        {
-            try
-            {
-                _ = _reader.Read();
-                Assert.Fail();
-            }
-            catch (ObjectDisposedException ex)
-            {
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual(typeof(SftpFileReader).FullName, ex.ObjectName);
-            }
-        }
-
-        [TestMethod]
-        public void InvokeOfReadAheadCallbackShouldCompleteImmediately()
-        {
-            Assert.IsNotNull(_readAsyncCallback);
-
-            _readAsyncCallback(new SftpReadAsyncResult(null, null));
-        }
-
-        [TestMethod]
-        public void BeginCloseOnSftpSessionShouldHaveBeenInvokedOnce()
-        {
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-        }
-    }
-}

+ 0 - 141
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Dispose_SftpSessionIsOpen_EndCloseThrowsException.cs

@@ -1,141 +0,0 @@
-using System;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_Dispose_SftpSessionIsOpen_EndCloseThrowsException : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private SftpFileReader _reader;
-        private AsyncCallback _readAsyncCallback;
-        private ManualResetEvent _beginReadInvoked;
-        private EventWaitHandle _disposeCompleted;
-
-        [TestCleanup]
-        public void TearDown()
-        {
-            _beginReadInvoked?.Dispose();
-            _disposeCompleted?.Dispose();
-        }
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _fileSize = 5000;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-            _beginReadInvoked = new ManualResetEvent(false);
-            _disposeCompleted = new ManualResetEvent(false);
-            _readAsyncCallback = null;
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                               .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                                   {
-                                       _waitHandleArray[0] = disposingWaitHandle;
-                                       _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                                       return _waitHandleArray;
-                                   });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback(() =>
-                                   {
-                                       // harden test by making sure that we've invoked BeginRead before Dispose is invoked
-                                       _ = _beginReadInvoked.Set();
-                                   })
-                               .Returns<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       _readAsyncCallback = callback;
-                                       return null;
-                                   })
-                               .Callback(() =>
-                                   {
-                                       // wait until Dispose has been invoked on reader to allow us to harden test, and
-                                       // verify whether Dispose will prevent us from entering the read-ahead loop again
-                                       _ = _waitHandleArray[0].WaitOne();
-                                   });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.IsOpen)
-                               .Returns(true);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginClose(_handle, null, null))
-                               .Returns(_closeAsyncResult);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.EndClose(_closeAsyncResult))
-                               .Throws(new SshException());
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            Assert.IsTrue(_beginReadInvoked.WaitOne(5000));
-            _reader.Dispose();
-        }
-
-        [TestMethod]
-        public void ReadAfterDisposeShouldThrowObjectDisposedException()
-        {
-            try
-            {
-                _ = _reader.Read();
-                Assert.Fail();
-            }
-            catch (ObjectDisposedException ex)
-            {
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual(typeof(SftpFileReader).FullName, ex.ObjectName);
-            }
-        }
-
-        [TestMethod]
-        public void InvokeOfReadAheadCallbackShouldCompleteImmediately()
-        {
-            Assert.IsNotNull(_readAsyncCallback);
-
-            _readAsyncCallback(new SftpReadAsyncResult(null, null));
-        }
-
-        [TestMethod]
-        public void EndCloseOnSftpSessionShouldHaveBeenInvokedOnce()
-        {
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-    }
-}

+ 0 - 168
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsComplete.cs

@@ -1,168 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_LastChunkBeforeEofIsComplete : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpReadAsyncResult _readAsyncResultBeyondEof;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private byte[] _chunk1;
-        private byte[] _chunk2;
-        private byte[] _chunk3;
-        private SftpFileReader _reader;
-        private byte[] _actualChunk1;
-        private byte[] _actualChunk2;
-        private byte[] _actualChunk3;
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _chunk1 = CreateByteArray(random, ChunkLength);
-            // chunk is less than the requested length, but - together with chunk 1 - contains all data up to the EOF
-            _chunk2 = CreateByteArray(random, ChunkLength - 10);
-            _chunk3 = new byte[0];
-            _fileSize = _chunk1.Length + _chunk2.Length;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-            _readAsyncResultBeyondEof = new SftpReadAsyncResult(null, null);
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                           .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                           {
-                               _waitHandleArray[0] = disposingWaitHandle;
-                               _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                               return _waitHandleArray;
-                           });
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                            .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                var asyncResult = new SftpReadAsyncResult(callback, state);
-                                asyncResult.SetAsCompleted(_chunk1, false);
-                            })
-                            .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                            .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                var asyncResult = new SftpReadAsyncResult(callback, state);
-                                asyncResult.SetAsCompleted(_chunk2, false);
-                            })
-                            .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, null, It.IsAny<BufferedRead>()))
-                           .Returns(_readAsyncResultBeyondEof);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.EndRead(_readAsyncResultBeyondEof))
-                           .Returns(_chunk3);
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 15, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            _actualChunk1 = _reader.Read();
-            _actualChunk2 = _reader.Read();
-            _actualChunk3 = _reader.Read();
-        }
-
-        [TestMethod]
-        public void FirstReadShouldReturnChunk1()
-        {
-            Assert.IsNotNull(_actualChunk1);
-            Assert.AreSame(_chunk1, _actualChunk1);
-        }
-
-        [TestMethod]
-        public void SecondReadShouldReturnChunk2()
-        {
-            Assert.IsNotNull(_actualChunk2);
-            Assert.AreSame(_chunk2, _actualChunk2);
-        }
-
-        [TestMethod]
-        public void ThirdReadShouldReturnChunk3()
-        {
-            Assert.IsNotNull(_actualChunk3);
-            Assert.AreSame(_chunk3, _actualChunk3);
-        }
-
-        [TestMethod]
-        public void ReadAfterEndOfFileShouldThrowSshException()
-        {
-            try
-            {
-                _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual("Attempting to read beyond the end of the file.", ex.Message);
-            }
-        }
-
-        [TestMethod]
-        public void DisposeShouldCloseHandleAndCompleteImmediately()
-        {
-            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
-
-            var stopwatch = Stopwatch.StartNew();
-            _reader.Dispose();
-            stopwatch.Stop();
-
-            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
-
-            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-    }
-}

+ 0 - 167
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_LastChunkBeforeEofIsPartial.cs

@@ -1,167 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_LastChunkBeforeEofIsPartial : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private byte[] _chunk1;
-        private byte[] _chunk2;
-        private byte[] _chunk3;
-        private SftpFileReader _reader;
-        private byte[] _actualChunk1;
-        private byte[] _actualChunk2;
-        private byte[] _actualChunk3;
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _chunk1 = CreateByteArray(random, ChunkLength);
-            _chunk2 = CreateByteArray(random, ChunkLength);
-            _chunk3 = new byte[0];
-            _fileSize = _chunk1.Length + _chunk2.Length;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                           .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                           {
-                               _waitHandleArray[0] = disposingWaitHandle;
-                               _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                               return _waitHandleArray;
-                           });
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                            .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                var asyncResult = new SftpReadAsyncResult(callback, state);
-                                asyncResult.SetAsCompleted(_chunk1, false);
-                            })
-                            .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                            .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                var asyncResult = new SftpReadAsyncResult(callback, state);
-                                asyncResult.SetAsCompleted(_chunk2, false);
-                            })
-                            .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                            .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                var asyncResult = new SftpReadAsyncResult(callback, state);
-                                asyncResult.SetAsCompleted(_chunk3, false);
-                            })
-                            .Returns((SftpReadAsyncResult)null);
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 15, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            _actualChunk1 = _reader.Read();
-            _actualChunk2 = _reader.Read();
-            _actualChunk3 = _reader.Read();
-        }
-
-        [TestMethod]
-        public void FirstReadShouldReturnChunk1()
-        {
-            Assert.IsNotNull(_actualChunk1);
-            Assert.AreSame(_chunk1, _actualChunk1);
-        }
-
-        [TestMethod]
-        public void SecondReadShouldReturnChunk2()
-        {
-            Assert.IsNotNull(_actualChunk2);
-            Assert.AreSame(_chunk2, _actualChunk2);
-        }
-
-        [TestMethod]
-        public void ThirdReadShouldReturnChunk3()
-        {
-            Assert.IsNotNull(_actualChunk3);
-            Assert.AreSame(_chunk3, _actualChunk3);
-        }
-
-        [TestMethod]
-        public void ReadAfterEndOfFileShouldThrowSshException()
-        {
-            try
-            {
-                _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual("Attempting to read beyond the end of the file.", ex.Message);
-            }
-        }
-
-        [TestMethod]
-        public void DisposeShouldCloseHandleAndCompleteImmediately()
-        {
-            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
-
-            var stopwatch = Stopwatch.StartNew();
-            _reader.Dispose();
-            stopwatch.Stop();
-
-            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
-
-            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-    }
-}

+ 0 - 323
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsNotReached.cs

@@ -1,323 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsNotReached : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private byte[] _chunk1;
-        private byte[] _chunk2;
-        private byte[] _chunk2CatchUp1;
-        private byte[] _chunk2CatchUp2;
-        private byte[] _chunk3;
-        private byte[] _chunk4;
-        private byte[] _chunk5;
-        private SftpFileReader _reader;
-        private byte[] _actualChunk1;
-        private byte[] _actualChunk2;
-        private byte[] _actualChunk3;
-        private ManualResetEvent _chunk1BeginRead;
-        private ManualResetEvent _chunk2BeginRead;
-        private ManualResetEvent _chunk3BeginRead;
-        private ManualResetEvent _chunk4BeginRead;
-        private ManualResetEvent _chunk5BeginRead;
-        private ManualResetEvent _waitBeforeChunk6;
-        private ManualResetEvent _chunk6BeginRead;
-        private byte[] _actualChunk4;
-        private byte[] _actualChunk2CatchUp1;
-        private byte[] _actualChunk2CatchUp2;
-        private byte[] _chunk6;
-        private byte[] _actualChunk5;
-        private byte[] _actualChunk6;
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 3);
-            _chunk1 = CreateByteArray(random, ChunkLength);
-            _chunk2 = CreateByteArray(random, ChunkLength - 17);
-            _chunk2CatchUp1 = CreateByteArray(random, 10);
-            _chunk2CatchUp2 = CreateByteArray(random, 7);
-            _chunk3 = CreateByteArray(random, ChunkLength);
-            _chunk4 = CreateByteArray(random, ChunkLength);
-            _chunk5 = CreateByteArray(random, ChunkLength);
-            _chunk6 = new byte[0];
-            _chunk1BeginRead = new ManualResetEvent(false);
-            _chunk2BeginRead = new ManualResetEvent(false);
-            _chunk3BeginRead = new ManualResetEvent(false);
-            _chunk4BeginRead = new ManualResetEvent(false);
-            _chunk5BeginRead = new ManualResetEvent(false);
-            _waitBeforeChunk6 = new ManualResetEvent(false);
-            _chunk6BeginRead = new ManualResetEvent(false);
-            _fileSize = _chunk1.Length + _chunk2.Length + _chunk2CatchUp1.Length + _chunk2CatchUp2.Length + _chunk3.Length + _chunk4.Length + _chunk5.Length;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                               .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                                   {
-                                       _waitHandleArray[0] = disposingWaitHandle;
-                                       _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                                       return _waitHandleArray;
-                                   });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       _ = _chunk1BeginRead.Set();
-                                       var asyncResult = new SftpReadAsyncResult(callback, state);
-                                       asyncResult.SetAsCompleted(_chunk1, false);
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       _ = _chunk2BeginRead.Set();
-                                       var asyncResult = new SftpReadAsyncResult(callback, state);
-                                       asyncResult.SetAsCompleted(_chunk2, false);
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       _ = _chunk3BeginRead.Set();
-                                       var asyncResult = new SftpReadAsyncResult(callback, state);
-                                       asyncResult.SetAsCompleted(_chunk3, false);
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 3 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       _ = _chunk4BeginRead.Set();
-                                       var asyncResult = new SftpReadAsyncResult(callback, state);
-                                       asyncResult.SetAsCompleted(_chunk4, false);
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 4 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       _ = _chunk5BeginRead.Set();
-                                       var asyncResult = new SftpReadAsyncResult(callback, state);
-                                       asyncResult.SetAsCompleted(_chunk5, false);
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Callback(() => _waitBeforeChunk6.Set())
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.RequestRead(_handle, (2 * ChunkLength) - 17, 17))
-                               .Returns(_chunk2CatchUp1);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.RequestRead(_handle, (2 * ChunkLength) - 7, 7))
-                               .Returns(_chunk2CatchUp2);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 5 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       _ = _chunk6BeginRead.Set();
-                                       var asyncResult = new SftpReadAsyncResult(callback, state);
-                                       asyncResult.SetAsCompleted(_chunk6, false);
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 3, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            // reader is configured to read-ahead max. 3 chunks, so chunk4 should not have been read
-            Assert.IsFalse(_chunk4BeginRead.WaitOne(0));
-            // consume chunk 1
-            _actualChunk1 = _reader.Read();
-            // consuming chunk1 allows chunk4 to be read-ahead
-            Assert.IsTrue(_chunk4BeginRead.WaitOne(200));
-            // verify that chunk5 has not yet been read-ahead
-            Assert.IsFalse(_chunk5BeginRead.WaitOne(0));
-            // consume chunk 2
-            _actualChunk2 = _reader.Read();
-            // consuming chunk2 allows chunk5 to be read-ahead
-            Assert.IsTrue(_chunk5BeginRead.WaitOne(200));
-            // pauze until the read-ahead has started waiting a semaphore to become available
-            Assert.IsTrue(_waitBeforeChunk6.WaitOne(200));
-            // consume remaining parts of chunk 2
-            _actualChunk2CatchUp1 = _reader.Read();
-            _actualChunk2CatchUp2 = _reader.Read();
-            // verify that chunk6 has not yet been read-ahead
-            Assert.IsFalse(_chunk6BeginRead.WaitOne(0));
-            // consume chunk 3
-            _actualChunk3 = _reader.Read();
-            // consuming chunk3 allows chunk6 to be read-ahead
-            Assert.IsTrue(_chunk6BeginRead.WaitOne(200));
-            // consume chunk 4
-            _actualChunk4 = _reader.Read();
-            // consume chunk 5
-            _actualChunk5 = _reader.Read();
-            // consume chunk 6
-            _actualChunk6 = _reader.Read();
-        }
-
-        [TestMethod]
-        public void FirstReadShouldReturnChunk1()
-        {
-            Assert.IsNotNull(_actualChunk1);
-            Assert.AreSame(_chunk1, _actualChunk1);
-        }
-
-        [TestMethod]
-        public void SecondReadShouldReturnChunk2()
-        {
-            Assert.IsNotNull(_actualChunk2);
-            Assert.AreSame(_chunk2, _actualChunk2);
-        }
-
-        [TestMethod]
-        public void ThirdReadShouldReturnChunk2CatchUp1()
-        {
-            Assert.IsNotNull(_actualChunk2CatchUp1);
-            Assert.AreSame(_chunk2CatchUp1, _actualChunk2CatchUp1);
-        }
-
-        [TestMethod]
-        public void FourthReadShouldReturnChunk2CatchUp2()
-        {
-            Assert.IsNotNull(_actualChunk2CatchUp2);
-            Assert.AreSame(_chunk2CatchUp2, _actualChunk2CatchUp2);
-        }
-
-        [TestMethod]
-        public void FifthReadShouldReturnChunk3()
-        {
-            Assert.IsNotNull(_actualChunk3);
-            Assert.AreSame(_chunk3, _actualChunk3);
-        }
-
-        [TestMethod]
-        public void SixthReadShouldReturnChunk4()
-        {
-            Assert.IsNotNull(_actualChunk4);
-            Assert.AreSame(_chunk4, _actualChunk4);
-        }
-
-        [TestMethod]
-        public void SeventhReadShouldReturnChunk5()
-        {
-            Assert.IsNotNull(_actualChunk5);
-            Assert.AreSame(_chunk5, _actualChunk5);
-        }
-
-        [TestMethod]
-        public void EightReadShouldReturnChunk6()
-        {
-            Assert.IsNotNull(_actualChunk6);
-            Assert.AreSame(_chunk6, _actualChunk6);
-        }
-
-        [TestMethod]
-        public void ReadAfterEndOfFileShouldThrowSshException()
-        {
-            try
-            {
-                _ = _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual("Attempting to read beyond the end of the file.", ex.Message);
-            }
-        }
-
-        [TestMethod]
-        public void DisposeShouldCloseHandleAndCompleteImmediately()
-        {
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.IsOpen)
-                               .Returns(true);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginClose(_handle, null, null))
-                               .Returns(_closeAsyncResult);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.EndClose(_closeAsyncResult));
-
-            var stopwatch = Stopwatch.StartNew();
-            _reader.Dispose();
-            stopwatch.Stop();
-
-            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
-
-            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-    }
-}

+ 0 - 207
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsReached.cs

@@ -1,207 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_PreviousChunkIsIncompleteAndEofIsReached : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private byte[] _chunk1;
-        private byte[] _chunk2;
-        private byte[] _chunk2CatchUp;
-        private byte[] _chunk3;
-        private SftpFileReader _reader;
-        private byte[] _actualChunk1;
-        private byte[] _actualChunk2;
-        private byte[] _actualChunk2CatchUp;
-        private byte[] _actualChunk3;
-        private ManualResetEvent _chunk1BeginRead;
-        private ManualResetEvent _chunk2BeginRead;
-        private ManualResetEvent _chunk3BeginRead;
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 3);
-            _chunk1 = CreateByteArray(random, ChunkLength);
-            _chunk2 = CreateByteArray(random, ChunkLength - 10);
-            _chunk2CatchUp = CreateByteArray(random, 10);
-            _chunk3 = new byte[0];
-            _chunk1BeginRead = new ManualResetEvent(false);
-            _chunk2BeginRead = new ManualResetEvent(false);
-            _chunk3BeginRead = new ManualResetEvent(false);
-            _fileSize = _chunk1.Length + _chunk2.Length + _chunk2CatchUp.Length + _chunk3.Length;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                               .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                                   {
-                                       _waitHandleArray[0] = disposingWaitHandle;
-                                       _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                                       return _waitHandleArray;
-                                   });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       _ = _chunk1BeginRead.Set();
-                                       var asyncResult = new SftpReadAsyncResult(callback, state);
-                                       asyncResult.SetAsCompleted(_chunk1, false);
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       _ = _chunk2BeginRead.Set();
-                                       var asyncResult = new SftpReadAsyncResult(callback, state);
-                                       asyncResult.SetAsCompleted(_chunk2, false);
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       _ = _chunk3BeginRead.Set();
-                                       var asyncResult = new SftpReadAsyncResult(callback, state);
-                                       asyncResult.SetAsCompleted(_chunk3, false);
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.RequestRead(_handle, (2 * ChunkLength) - 10, 10))
-                               .Returns(_chunk2CatchUp);
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 5, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            // consume chunk 1
-            _actualChunk1 = _reader.Read();
-            // consume chunk 2
-            _actualChunk2 = _reader.Read();
-            // wait until chunk3 has been read-ahead
-            Assert.IsTrue(_chunk3BeginRead.WaitOne(200));
-            // consume remaining parts of chunk 2
-            _actualChunk2CatchUp = _reader.Read();
-            // consume chunk 3
-            _actualChunk3 = _reader.Read();
-        }
-
-        [TestMethod]
-        public void FirstReadShouldReturnChunk1()
-        {
-            Assert.IsNotNull(_actualChunk1);
-            Assert.AreSame(_chunk1, _actualChunk1);
-        }
-
-        [TestMethod]
-        public void SecondReadShouldReturnChunk2()
-        {
-            Assert.IsNotNull(_actualChunk2);
-            Assert.AreSame(_chunk2, _actualChunk2);
-        }
-
-        [TestMethod]
-        public void ThirdReadShouldReturnChunk2CatchUp()
-        {
-            Assert.IsNotNull(_actualChunk2CatchUp);
-            Assert.AreSame(_chunk2CatchUp, _actualChunk2CatchUp);
-        }
-
-        [TestMethod]
-        public void FourthReadShouldReturnChunk3()
-        {
-            Assert.IsNotNull(_actualChunk3);
-            Assert.AreSame(_chunk3, _actualChunk3);
-        }
-
-        [TestMethod]
-        public void ReadAfterEndOfFileShouldThrowSshException()
-        {
-            try
-            {
-                _ = _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                Assert.IsNull(ex.InnerException);
-                Assert.AreEqual("Attempting to read beyond the end of the file.", ex.Message);
-            }
-        }
-
-        [TestMethod]
-        public void DisposeShouldCloseHandleAndCompleteImmediately()
-        {
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.IsOpen)
-                               .Returns(true);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginClose(_handle, null, null))
-                               .Returns(_closeAsyncResult);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.EndClose(_closeAsyncResult));
-
-            var stopwatch = Stopwatch.StartNew();
-            _reader.Dispose();
-            stopwatch.Stop();
-
-            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
-
-            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-    }
-}

+ 0 - 6
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadBeginReadException.cs

@@ -1,6 +0,0 @@
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    class SftpFileReaderTest_ReadAheadBeginReadException
-    {
-    }
-}

+ 0 - 213
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadEndInvokeException_DiscardsFurtherReadAheads.cs

@@ -1,213 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    /// <summary>
-    /// Runs a reader with max. 2 pending reads.
-    /// The read-ahead of chunk1 starts followed by the read-ahead of chunk2.
-    /// The read-ahead of chunk1 completes successfully and the resulting chunk is read.
-    /// The read of this first chunk allows a third ahead-head to start.
-    /// The second read-ahead uses signals to forcefully block a failure completion until the read
-    /// ahead of the third chunk has completed and the semaphore is waiting for a slot to start
-    /// the read-ahead of chunk4.
-    /// The second read does not consume check3 as it is out of order, but instead waits for
-    /// the outcome of the read-ahead of chunk2.
-    /// 
-    /// The completion with exception of chunk2 causes the second read to throw that same exception, and
-    /// signals the semaphore that was waiting to start the read-ahead of chunk4. However, due to the fact
-    /// that chunk2 completed with an exception, the read-ahead loop is stopped.
-    /// </summary>
-    [TestClass]
-    public class SftpFileReaderTest_ReadAheadEndInvokeException_DiscardsFurtherReadAheads : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private byte[] _chunk1;
-        private byte[] _chunk3;
-        private ManualResetEvent _readAheadChunk2Completed;
-        private ManualResetEvent _readAheadChunk3Completed;
-        private ManualResetEvent _waitingForSemaphoreAfterCompletingChunk3;
-        private SftpFileReader _reader;
-        private SshException _exception;
-        private SshException _actualException;
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _chunk1 = CreateByteArray(random, ChunkLength);
-            _chunk3 = CreateByteArray(random, ChunkLength);
-            _fileSize = 4 * ChunkLength;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-
-            _readAheadChunk2Completed = new ManualResetEvent(false);
-            _readAheadChunk3Completed = new ManualResetEvent(false);
-            _waitingForSemaphoreAfterCompletingChunk3 = new ManualResetEvent(false);
-
-            _exception = new SshException();
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                           .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                           {
-                               _waitHandleArray[0] = disposingWaitHandle;
-                               _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                               return _waitHandleArray;
-                           });
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                           .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                           {
-                               var asyncResult = new SftpReadAsyncResult(callback, state);
-                               asyncResult.SetAsCompleted(_chunk1, false);
-                           })
-                           .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                            .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                ThreadAbstraction.ExecuteThread(() =>
-                                {
-                                    // wait until the read-ahead for chunk3 has completed; this should allow
-                                    // the read-ahead of chunk4 to start
-                                    _readAheadChunk3Completed.WaitOne(TimeSpan.FromSeconds(3));
-                                    // wait until the semaphore wait to start with chunk4 has started
-                                    _waitingForSemaphoreAfterCompletingChunk3.WaitOne(TimeSpan.FromSeconds(7));
-                                    // complete async read of chunk2 with exception
-                                    var asyncResult = new SftpReadAsyncResult(callback, state);
-                                    asyncResult.SetAsCompleted(_exception, false);
-                                    // signal that read-ahead of chunk 2 has completed
-                                    _readAheadChunk2Completed.Set();
-                                });
-                            })
-                           .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                            .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                var asyncResult = new SftpReadAsyncResult(callback, state);
-                                asyncResult.SetAsCompleted(_chunk3, false);
-                                // signal that we've completed the read-ahead for chunk3
-                                _readAheadChunk3Completed.Set();
-                            })
-                            .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Callback(() => _waitingForSemaphoreAfterCompletingChunk3.Set())
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 2, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            _reader.Read();
-
-            try
-            {
-                _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                _actualException = ex;
-            }
-        }
-
-        [TestMethod]
-        public void ReadOfSecondChunkShouldThrowExceptionThatOccurredInReadAhead()
-        {
-            Assert.IsNotNull(_actualException);
-            Assert.AreSame(_exception, _actualException);
-        }
-
-        [TestMethod]
-        public void ReahAheadOfChunk3ShouldHaveStarted()
-        {
-            SftpSessionMock.Verify(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()), Times.Once);
-        }
-
-        [TestMethod]
-        public void ReadAfterReadAheadExceptionShouldRethrowExceptionThatOccurredInReadAhead()
-        {
-            try
-            {
-                _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                Assert.AreSame(_exception, ex);
-            }
-        }
-
-        [TestMethod]
-        public void WaitAnyOFSftpSessionShouldHaveBeenInvokedFourTimes()
-        {
-            SftpSessionMock.Verify(p => p.WaitAny(_waitHandleArray, _operationTimeout), Times.Exactly(4));
-        }
-
-        [TestMethod]
-        public void DisposeShouldCloseHandleAndCompleteImmediately()
-        {
-            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
-
-            var stopwatch = Stopwatch.StartNew();
-            _reader.Dispose();
-            stopwatch.Stop();
-
-            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
-
-            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-    }
-}

+ 0 - 189
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadAheadEndInvokeException_PreventsFurtherReadAheads.cs

@@ -1,189 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Abstractions;
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_ReadAheadEndInvokeException_PreventsFurtherReadAheads : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private byte[] _chunk1;
-        private SftpFileReader _reader;
-        private ManualResetEvent _readAheadChunk2;
-        private ManualResetEvent _readChunk2;
-        private SshException _exception;
-        private SshException _actualException;
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _chunk1 = CreateByteArray(random, ChunkLength);
-            _fileSize = 3 * _chunk1.Length;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-
-            _readAheadChunk2 = new ManualResetEvent(false);
-            _readChunk2 = new ManualResetEvent(false);
-
-            _exception = new SshException();
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                               .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                                   {
-                                       _waitHandleArray[0] = disposingWaitHandle;
-                                       _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                                       return _waitHandleArray;
-                                   });
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       var asyncResult = new SftpReadAsyncResult(callback, state);
-                                       asyncResult.SetAsCompleted(_chunk1, false);
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                               .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                   {
-                                       ThreadAbstraction.ExecuteThread(() =>
-                                           {
-                                               // signal that we're in the read-ahead for chunk2
-                                               _ = _readAheadChunk2.Set();
-                                               // wait for client to start reading this chunk
-                                               _ = _readChunk2.WaitOne(TimeSpan.FromSeconds(5));
-                                               // sleep a short time to make sure the client is in the blocking wait
-                                               Thread.Sleep(500);
-                                               // complete async read of chunk2 with exception
-                                               var asyncResult = new SftpReadAsyncResult(callback, state);
-                                               asyncResult.SetAsCompleted(_exception, false);
-                                           });
-                                   })
-                               .Returns((SftpReadAsyncResult)null);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.OperationTimeout)
-                               .Returns(_operationTimeout);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                               .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            // use a max. read-ahead of 1 to allow us to verify that the next read-ahead is not done
-            // when a read-ahead has failed
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            _ = _reader.Read();
-
-            // wait until SftpFileReader has starting reading ahead chunk 2
-            Assert.IsTrue(_readAheadChunk2.WaitOne(TimeSpan.FromSeconds(5)));
-            // signal that we are about to read chunk 2
-            _ = _readChunk2.Set();
-
-            try
-            {
-                _ = _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                _actualException = ex;
-            }
-        }
-
-        [TestMethod]
-        public void ReadOfSecondChunkShouldThrowExceptionThatOccurredInReadAhead()
-        {
-            Assert.IsNotNull(_actualException);
-            Assert.AreSame(_exception, _actualException);
-        }
-
-        [TestMethod]
-        public void ReadAfterReadAheadExceptionShouldRethrowExceptionThatOccurredInReadAhead()
-        {
-            try
-            {
-                _ = _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                Assert.AreSame(_exception, ex);
-            }
-        }
-
-        [TestMethod]
-        public void DisposeShouldCloseHandleAndCompleteImmediately()
-        {
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.IsOpen)
-                               .Returns(true);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.BeginClose(_handle, null, null))
-                               .Returns(_closeAsyncResult);
-            _ = SftpSessionMock.InSequence(_seq)
-                               .Setup(p => p.EndClose(_closeAsyncResult));
-
-            var stopwatch = Stopwatch.StartNew();
-            _reader.Dispose();
-            stopwatch.Stop();
-
-            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
-
-            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-
-        [TestMethod]
-        public void ExceptionInReadAheadShouldPreventFurtherReadAheads()
-        {
-            SftpSessionMock.Verify(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()), Times.Never);
-        }
-    }
-}

+ 0 - 6
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadBackBeginReadException.cs

@@ -1,6 +0,0 @@
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    class SftpFileReaderTest_ReadBackBeginReadException
-    {
-    }
-}

+ 0 - 6
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_ReadBackEndInvokeException.cs

@@ -1,6 +0,0 @@
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    class SftpFileReaderTest_ReadBackEndInvokeException
-    {
-    }
-}

+ 0 - 172
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInBeginRead.cs

@@ -1,172 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_Read_ReadAheadExceptionInBeginRead : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private byte[] _chunk1;
-        private byte[] _chunk2;
-        private SftpFileReader _reader;
-        private ManualResetEvent _readAheadChunk3;
-        private ManualResetEvent _readChunk3;
-        private SshException _exception;
-        private SshException _actualException;
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _chunk1 = CreateByteArray(random, ChunkLength);
-            _chunk2 = CreateByteArray(random, ChunkLength);
-            _fileSize = _chunk1.Length + _chunk2.Length + 1;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-
-            _readAheadChunk3 = new ManualResetEvent(false);
-            _readChunk3 = new ManualResetEvent(false);
-
-            _exception = new SshException();
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                           .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                           {
-                               _waitHandleArray[0] = disposingWaitHandle;
-                               _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                               return _waitHandleArray;
-                           });
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                           .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                                {
-                                    var asyncResult = new SftpReadAsyncResult(callback, state);
-                                    asyncResult.SetAsCompleted(_chunk1, false);
-                                })
-                           .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                            .Setup(p => p.BeginRead(_handle, ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                var asyncResult = new SftpReadAsyncResult(callback, state);
-                                asyncResult.SetAsCompleted(_chunk2, false);
-                            })
-                            .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                            .Setup(p => p.BeginRead(_handle, 2 * ChunkLength, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                _readAheadChunk3.Set();
-                                _readChunk3.WaitOne(TimeSpan.FromSeconds(5));
-                                // sleep a short time to make sure the client is in the blocking wait
-                                Thread.Sleep(500);
-                            })
-                            .Throws(_exception);
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 3, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            _reader.Read();
-            _reader.Read();
-
-            // wait until we've the SftpFileReader has starting reading ahead chunk 3
-            Assert.IsTrue(_readAheadChunk3.WaitOne(TimeSpan.FromSeconds(5)));
-            // signal that we are about to read chunk 3
-            _readChunk3.Set();
-
-            try
-            {
-                _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                _actualException = ex;
-            }
-        }
-
-        [TestMethod]
-        public void ReadOfThirdChunkShouldThrowExceptionThatOccurredInReadAhead()
-        {
-            Assert.IsNotNull(_actualException);
-            Assert.AreSame(_exception, _actualException);
-        }
-
-        [TestMethod]
-        public void ReadAfterReadAheadExceptionShouldRethrowExceptionThatOccurredInReadAhead()
-        {
-            try
-            {
-                _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                Assert.AreSame(_exception, ex);
-            }
-        }
-
-        [TestMethod]
-        public void DisposeShouldCloseHandleAndCompleteImmediately()
-        {
-            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
-
-            var stopwatch = Stopwatch.StartNew();
-            _reader.Dispose();
-            stopwatch.Stop();
-
-            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
-
-            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-    }
-}

+ 0 - 144
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_ChunkAvailable.cs

@@ -1,144 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_ChunkAvailable : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private byte[] _chunk1;
-        private byte[] _chunk2;
-        private SftpFileReader _reader;
-        private SshException _exception;
-        private ManualResetEvent _exceptionSignaled;
-        private SshException _actualException;
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _chunk1 = CreateByteArray(random, ChunkLength);
-            _chunk2 = CreateByteArray(random, ChunkLength);
-            _fileSize = _chunk1.Length + _chunk2.Length + 1;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-
-            _exception = new SshException();
-            _exceptionSignaled = new ManualResetEvent(false);
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                           .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                               {
-                                   _waitHandleArray[0] = disposingWaitHandle;
-                                   _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                                   return _waitHandleArray;
-                               });
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                            .Callback<byte[], ulong, uint, AsyncCallback, object>((handle, offset, length, callback, state) =>
-                            {
-                                var asyncResult = new SftpReadAsyncResult(callback, state);
-                                asyncResult.SetAsCompleted(_chunk1, false);
-                            })
-                           .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Callback(() => _exceptionSignaled.Set())
-                           .Throws(_exception);
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 2, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            // wait for the exception to be signaled by the second call to WaitAny
-            _exceptionSignaled.WaitOne(5000);
-            // allow a little time to allow SftpFileReader to process exception
-            Thread.Sleep(100);
-            try
-            {
-                _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                _actualException = ex;
-            }
-        }
-
-        [TestMethod]
-        public void ReadShouldHaveRethrownExceptionThrownByWaitAny()
-        {
-            Assert.IsNotNull(_actualException);
-            Assert.AreSame(_exception, _actualException);
-        }
-
-        [TestMethod]
-        public void ReadShouldRethrowExceptionThrownByWaitAny()
-        {
-            try
-            {
-                _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                Assert.AreSame(_exception, ex);
-            }
-        }
-
-        [TestMethod]
-        public void DisposeShouldCloseHandleAndCompleteImmediately()
-        {
-            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
-
-            var stopwatch = Stopwatch.StartNew();
-            _reader.Dispose();
-            stopwatch.Stop();
-
-            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
-
-            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-    }
-}

+ 0 - 128
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_NoChunkAvailable.cs

@@ -1,128 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Sftp;
-
-using BufferedRead = Renci.SshNet.Sftp.SftpFileReader.BufferedRead;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    [TestClass]
-    public class SftpFileReaderTest_Read_ReadAheadExceptionInWaitOnHandle_NoChunkAvailable : SftpFileReaderTestBase
-    {
-        private const int ChunkLength = 32 * 1024;
-
-        private MockSequence _seq;
-        private byte[] _handle;
-        private int _fileSize;
-        private WaitHandle[] _waitHandleArray;
-        private int _operationTimeout;
-        private SftpCloseAsyncResult _closeAsyncResult;
-        private SftpFileReader _reader;
-        private SshException _exception;
-        private SshException _actualException;
-
-        protected override void SetupData()
-        {
-            var random = new Random();
-
-            _handle = CreateByteArray(random, 5);
-            _fileSize = 1234;
-            _waitHandleArray = new WaitHandle[2];
-            _operationTimeout = random.Next(10000, 20000);
-            _closeAsyncResult = new SftpCloseAsyncResult(null, null);
-
-            _exception = new SshException();
-        }
-
-        protected override void SetupMocks()
-        {
-            _seq = new MockSequence();
-
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.CreateWaitHandleArray(It.IsNotNull<WaitHandle>(), It.IsNotNull<WaitHandle>()))
-                           .Returns<WaitHandle, WaitHandle>((disposingWaitHandle, semaphoreAvailableWaitHandle) =>
-                           {
-                               _waitHandleArray[0] = disposingWaitHandle;
-                               _waitHandleArray[1] = semaphoreAvailableWaitHandle;
-                               return _waitHandleArray;
-                           });
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Returns(() => WaitAny(_waitHandleArray, _operationTimeout));
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.BeginRead(_handle, 0, ChunkLength, It.IsNotNull<AsyncCallback>(), It.IsAny<BufferedRead>()))
-                           .Returns((SftpReadAsyncResult)null);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.OperationTimeout).Returns(_operationTimeout);
-            SftpSessionMock.InSequence(_seq)
-                           .Setup(p => p.WaitAny(_waitHandleArray, _operationTimeout))
-                           .Throws(_exception);
-        }
-
-        protected override void Arrange()
-        {
-            base.Arrange();
-
-            _reader = new SftpFileReader(_handle, SftpSessionMock.Object, ChunkLength, 1, _fileSize);
-        }
-
-        protected override void Act()
-        {
-            try
-            {
-                _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                _actualException = ex;
-            }
-        }
-
-        [TestMethod]
-        public void ReadShouldHaveRethrownExceptionThrownByWaitOnHandle()
-        {
-            Assert.IsNotNull(_actualException);
-            Assert.AreSame(_exception, _actualException);
-        }
-
-        [TestMethod]
-        public void ReadShouldRethrowExceptionThrownByWaitOnHandle()
-        {
-            try
-            {
-                _reader.Read();
-                Assert.Fail();
-            }
-            catch (SshException ex)
-            {
-                Assert.AreSame(_exception, ex);
-            }
-        }
-
-        [TestMethod]
-        public void DisposeShouldCloseHandleAndCompleteImmediately()
-        {
-            SftpSessionMock.InSequence(_seq).Setup(p => p.IsOpen).Returns(true);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.BeginClose(_handle, null, null)).Returns(_closeAsyncResult);
-            SftpSessionMock.InSequence(_seq).Setup(p => p.EndClose(_closeAsyncResult));
-
-            var stopwatch = Stopwatch.StartNew();
-            _reader.Dispose();
-            stopwatch.Stop();
-
-            Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200, "Dispose took too long to complete: " + stopwatch.ElapsedMilliseconds);
-
-            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
-            SftpSessionMock.Verify(p => p.BeginClose(_handle, null, null), Times.Once);
-            SftpSessionMock.Verify(p => p.EndClose(_closeAsyncResult), Times.Once);
-        }
-    }
-}

+ 0 - 70
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamAsyncTestBase.cs

@@ -1,70 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Moq;
-
-using Renci.SshNet.Sftp;
-
-namespace Renci.SshNet.Tests.Classes.Sftp
-{
-    public abstract class SftpFileStreamAsyncTestBase
-    {
-        internal Mock<ISftpSession> SftpSessionMock;
-        protected MockSequence MockSequence;
-
-        protected virtual Task ArrangeAsync()
-        {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
-            return Task.CompletedTask;
-        }
-
-        protected virtual void SetupData()
-        {
-            MockSequence = new MockSequence();
-        }
-
-        protected abstract void SetupMocks();
-
-        private void CreateMocks()
-        {
-            SftpSessionMock = new Mock<ISftpSession>(MockBehavior.Strict);
-        }
-
-        [TestInitialize]
-        public async Task SetUpAsync()
-        {
-            await ArrangeAsync();
-            await ActAsync();
-        }
-
-        protected abstract Task ActAsync();
-
-        protected byte[] GenerateRandom(int length)
-        {
-            return GenerateRandom(length, new Random());
-        }
-
-        protected byte[] GenerateRandom(int length, Random random)
-        {
-            var buffer = new byte[length];
-            random.NextBytes(buffer);
-            return buffer;
-        }
-
-        protected byte[] GenerateRandom(uint length)
-        {
-            return GenerateRandom(length, new Random());
-        }
-
-        protected byte[] GenerateRandom(uint length, Random random)
-        {
-            var buffer = new byte[length];
-            random.NextBytes(buffer);
-            return buffer;
-        }
-    }
-}

+ 49 - 4
test/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
+using Microsoft.Extensions.Logging.Abstractions;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 
 using Moq;
@@ -85,7 +86,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
                     SftpFileStream.Open(new Mock<ISftpSession>().Object, "file.txt", mode, access, bufferSize: 1024));
             }
 
-            Assert.AreEqual("mode", ex.ParamName);
+            Assert.AreEqual("access", ex.ParamName);
         }
 
         [TestMethod]
@@ -93,6 +94,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             var sessionMock = new Mock<ISftpSession>();
 
+            sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
             sessionMock.Setup(s => s.IsOpen).Returns(true);
 
             SetupRemoteSize(sessionMock, 128);
@@ -118,6 +120,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             var sessionMock = new Mock<ISftpSession>();
 
+            sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
             sessionMock.Setup(s => s.IsOpen).Returns(true);
 
             var s = SftpFileStream.Open(sessionMock.Object, "file.txt", FileMode.Open, FileAccess.Read, bufferSize: 1024);
@@ -135,7 +138,6 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             Assert.Throws<NotSupportedException>(() => s.SetLength(1024));
         }
 
-        [Ignore("TODO Currently throws EndOfStreamException in all cases.")]
         [TestMethod]
         [DataRow(-1, SeekOrigin.Begin)]
         [DataRow(-1, SeekOrigin.Current)]
@@ -144,6 +146,8 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             var sessionMock = new Mock<ISftpSession>();
 
+            sessionMock.Setup(s => s.CalculateOptimalReadLength(It.IsAny<uint>())).Returns<uint>(x => x);
+            sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
             sessionMock.Setup(s => s.IsOpen).Returns(true);
 
             SetupRemoteSize(sessionMock, 128);
@@ -155,7 +159,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
         private static void SetupRemoteSize(Mock<ISftpSession> sessionMock, long size)
         {
-            sessionMock.Setup(s => s.RequestFStat(It.IsAny<byte[]>(), It.IsAny<bool>())).Returns(new SftpFileAttributes(
+            sessionMock.Setup(s => s.RequestFStat(It.IsAny<byte[]>())).Returns(new SftpFileAttributes(
                 default, default, size: size, default, default, default, default
                 ));
         }
@@ -210,6 +214,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             var sessionMock = new Mock<ISftpSession>();
 
+            sessionMock.Setup(s => s.CalculateOptimalReadLength(It.IsAny<uint>())).Returns<uint>(x => x);
             sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
             sessionMock.Setup(s => s.IsOpen).Returns(true);
             SetupRemoteSize(sessionMock, 0);
@@ -246,12 +251,12 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             var sessionMock = new Mock<ISftpSession>();
 
+            sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
             sessionMock.Setup(s => s.IsOpen).Returns(true);
 
             var s = SftpFileStream.Open(sessionMock.Object, "file.txt", FileMode.Create, FileAccess.ReadWrite, bufferSize: 1024);
 
             Assert.IsTrue(s.CanRead);
-            Assert.IsTrue(s.CanSeek);
             Assert.IsTrue(s.CanWrite);
 
             s.Dispose();
@@ -276,6 +281,46 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             sessionMock.Verify(p => p.RequestClose(It.IsAny<byte[]>()), Times.Once);
         }
 
+        [TestMethod]
+        public void FstatFailure_DisablesSeek()
+        {
+            TestFstatFailure(fstat => fstat.Throws<SftpPermissionDeniedException>());
+        }
+
+        [TestMethod]
+        public void FstatSizeNotReturned_DisablesSeek()
+        {
+            TestFstatFailure(fstat => fstat.Returns(SftpFileAttributes.FromBytes([0, 0, 0, 0])));
+        }
+
+        private void TestFstatFailure(Action<Moq.Language.Flow.ISetup<ISftpSession, SftpFileAttributes>> fstatSetup)
+        {
+            var sessionMock = new Mock<ISftpSession>();
+
+            sessionMock.Setup(s => s.CalculateOptimalReadLength(It.IsAny<uint>())).Returns<uint>(x => x);
+            sessionMock.Setup(s => s.CalculateOptimalWriteLength(It.IsAny<uint>(), It.IsAny<byte[]>())).Returns<uint, byte[]>((x, _) => x);
+            sessionMock.Setup(p => p.SessionLoggerFactory).Returns(NullLoggerFactory.Instance);
+            sessionMock.Setup(s => s.IsOpen).Returns(true);
+
+            fstatSetup(sessionMock.Setup(s => s.RequestFStat(It.IsAny<byte[]>())));
+
+            var s = SftpFileStream.Open(sessionMock.Object, "file.txt", FileMode.Open, FileAccess.ReadWrite, bufferSize: 1024);
+
+            Assert.IsFalse(s.CanSeek);
+            Assert.IsTrue(s.CanRead);
+            Assert.IsTrue(s.CanWrite);
+
+            Assert.Throws<NotSupportedException>(() => s.Position);
+            Assert.Throws<NotSupportedException>(() => s.Length);
+            Assert.Throws<NotSupportedException>(() => s.Seek(0, SeekOrigin.Begin));
+            Assert.Throws<NotSupportedException>(() => s.SetLength(1024));
+
+            // Reads and writes still succeed.
+            _ = s.Read(new byte[16], 0, 16);
+            s.Write(new byte[16], 0, 16);
+            s.Flush();
+        }
+
         private static void VerifyRequestWrite(Mock<ISftpSession> sessionMock, ReadOnlyMemory<byte> newData, int serverOffset)
         {
             sessionMock.Verify(s => s.RequestWrite(