Browse Source

Merge pull request #199 from sshnet/sftpfilestream

SftpFileStream: No longer track write position separately
Gert Driesen 8 years ago
parent
commit
a9a38d7e94
15 changed files with 873 additions and 88 deletions
  1. 1 0
      src/Renci.SshNet.NETCore/Renci.SshNet.NETCore.csproj
  2. 26 2
      src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj
  3. 25 4
      src/Renci.SshNet.Tests/Classes/BaseClientTest_Connected_KeepAlivesNotSentConcurrently.cs
  4. 155 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndEqualToBufferSize.cs
  5. 148 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndLessThanBufferSize.cs
  6. 33 14
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesFromServerThanCount.cs
  7. 97 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetNegative.cs
  8. 88 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetPositive.cs
  9. 16 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetZero.cs
  10. 19 7
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_NoBuffering.cs
  11. 139 0
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_ReadBuffer.cs
  12. 8 0
      src/Renci.SshNet.Tests/Common/Extensions.cs
  13. 7 2
      src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
  14. 105 58
      src/Renci.SshNet/Sftp/SftpFileStream.cs
  15. 6 1
      src/Renci.SshNet/SftpClient.cs

+ 1 - 0
src/Renci.SshNet.NETCore/Renci.SshNet.NETCore.csproj

@@ -6,6 +6,7 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <AssemblyName>Renci.SshNet</AssemblyName>
     <AssemblyOriginatorKeyFile>../Renci.SshNet.snk</AssemblyOriginatorKeyFile>
+    <LangVersion>5</LangVersion>
     <SignAssembly>true</SignAssembly>
     <PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
     <GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>

+ 26 - 2
src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj

@@ -1202,8 +1202,29 @@
     <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_ReadByte_ReadMode_NoDataInWriteBufferAndNoDataInReadBuffer_LessDataThanReadBufferSizeAvailable.cs">
       <Link>Classes\Sftp\SftpFileStreamTest_ReadByte_ReadMode_NoDataInWriteBufferAndNoDataInReadBuffer_LessDataThanReadBufferSizeAvailable.cs</Link>
     </Compile>
-    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesThanCount.cs">
-      <Link>Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesThanCount.cs</Link>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndEqualToBufferSize.cs">
+      <Link>Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndEqualToBufferSize.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndLessThanBufferSize.cs">
+      <Link>Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndLessThanBufferSize.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesFromServerThanCount.cs">
+      <Link>Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesFromServerThanCount.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetNegative.cs">
+      <Link>Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetNegative.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetPositive.cs">
+      <Link>Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetPositive.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetZero.cs">
+      <Link>Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetZero.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_NoBuffering.cs">
+      <Link>Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_NoBuffering.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_ReadBuffer.cs">
+      <Link>Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_ReadBuffer.cs</Link>
     </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_SetLength_Closed.cs">
       <Link>Classes\Sftp\SftpFileStreamTest_SetLength_Closed.cs</Link>
@@ -1223,6 +1244,9 @@
     <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_SetLength_SessionOpen_FIleAccessWrite.cs">
       <Link>Classes\Sftp\SftpFileStreamTest_SetLength_SessionOpen_FIleAccessWrite.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileStreamTest_Write_SessionOpen_CountGreatherThanTwoTimesTheWriteBufferSize.cs">
+      <Link>Classes\Sftp\SftpFileStreamTest_Write_SessionOpen_CountGreatherThanTwoTimesTheWriteBufferSize.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\Sftp\SftpFileSystemInformationTest.cs">
       <Link>Classes\Sftp\SftpFileSystemInformationTest.cs</Link>
     </Compile>

+ 25 - 4
src/Renci.SshNet.Tests/Classes/BaseClientTest_Connected_KeepAlivesNotSentConcurrently.cs

@@ -11,8 +11,10 @@ namespace Renci.SshNet.Tests.Classes
     {
         private Mock<IServiceFactory> _serviceFactoryMock;
         private Mock<ISession> _sessionMock;
+        private MockSequence _mockSequence;
         private BaseClient _client;
         private ConnectionInfo _connectionInfo;
+        private ManualResetEvent _keepAliveSent;
 
         [TestInitialize]
         public void Setup()
@@ -24,20 +26,33 @@ namespace Renci.SshNet.Tests.Classes
         [TestCleanup]
         public void Cleanup()
         {
+            if (_client != null)
+            {
+                _sessionMock.InSequence(_mockSequence).Setup(p => p.OnDisconnecting());
+                _sessionMock.InSequence(_mockSequence).Setup(p => p.Dispose());
+                _client.Dispose();
+            }
         }
 
         protected void Arrange()
         {
             _connectionInfo = new ConnectionInfo("host", "user", new PasswordAuthenticationMethod("user", "pwd"));
+            _keepAliveSent = new ManualResetEvent(false);
 
             _serviceFactoryMock = new Mock<IServiceFactory>(MockBehavior.Strict);
             _sessionMock = new Mock<ISession>(MockBehavior.Strict);
 
-            _serviceFactoryMock.Setup(p => p.CreateSession(_connectionInfo)).Returns(_sessionMock.Object);
-            _sessionMock.Setup(p => p.Connect());
-            _sessionMock.Setup(p => p.TrySendMessage(It.IsAny<IgnoreMessage>()))
+            _mockSequence = new MockSequence();
+
+            _serviceFactoryMock.InSequence(_mockSequence).Setup(p => p.CreateSession(_connectionInfo)).Returns(_sessionMock.Object);
+            _sessionMock.InSequence(_mockSequence).Setup(p => p.Connect());
+            _sessionMock.InSequence(_mockSequence).Setup(p => p.TrySendMessage(It.IsAny<IgnoreMessage>()))
                 .Returns(true)
-                .Callback(() => Thread.Sleep(300));
+                .Callback(() =>
+                    {
+                        Thread.Sleep(300);
+                        _keepAliveSent.Set();
+                    });
 
             _client = new MyClient(_connectionInfo, false, _serviceFactoryMock.Object)
                 {
@@ -51,6 +66,12 @@ namespace Renci.SshNet.Tests.Classes
             // should keep-alive message be sent concurrently, then multiple keep-alive
             // message would be sent during this sleep period
             Thread.Sleep(200);
+
+            // disable further keep-alives
+            _client.KeepAliveInterval = Session.InfiniteTimeSpan;
+
+            // wait until keep-alive has been sent at least once
+            Assert.IsTrue(_keepAliveSent.WaitOne(500));
         }
 
         [TestMethod]

+ 155 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndEqualToBufferSize.cs

@@ -0,0 +1,155 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+using Renci.SshNet.Sftp;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndEqualToBufferSize : SftpFileStreamTestBase
+    {
+        private string _path;
+        private SftpFileStream _target;
+        private byte[] _handle;
+        private uint _bufferSize;
+        private uint _readBufferSize;
+        private uint _writeBufferSize;
+        private int _actual;
+        private byte[] _buffer;
+        private byte[] _serverData1;
+        private byte[] _serverData2;
+        private int _serverData1Length;
+        private int _serverData2Length;
+        private int _numberOfBytesToRead;
+
+        protected override void SetupData()
+        {
+            base.SetupData();
+
+            var random = new Random();
+            _path = random.Next().ToString();
+            _handle = GenerateRandom(5, random);
+            _bufferSize = (uint)random.Next(1, 1000);
+            _readBufferSize = 20;
+            _writeBufferSize = 500;
+
+            _numberOfBytesToRead = (int) _readBufferSize + 5; // greather than read buffer size
+            _buffer = new byte[_numberOfBytesToRead];
+            _serverData1Length = (int) _readBufferSize; // equal to read buffer size
+            _serverData1 = GenerateRandom(_serverData1Length, random);
+            _serverData2Length = (int) _readBufferSize; // equal to read buffer size
+            _serverData2 = GenerateRandom(_serverData2Length, random);
+
+            Assert.IsTrue(_serverData1Length < _numberOfBytesToRead && _serverData1Length == _readBufferSize);
+        }
+
+        protected override void SetupMocks()
+        {
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.RequestOpen(_path, Flags.Read, false))
+                .Returns(_handle);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
+                .Returns(_readBufferSize);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.CalculateOptimalWriteLength(_bufferSize, _handle))
+                .Returns(_writeBufferSize);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.IsOpen)
+                .Returns(true);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.RequestRead(_handle, 0UL, _readBufferSize))
+                .Returns(_serverData1);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.RequestRead(_handle, (ulong)_serverData1.Length, _readBufferSize))
+                .Returns(_serverData2);
+        }
+
+        [TestCleanup]
+        public void TearDown()
+        {
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.RequestClose(_handle));
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _target = new SftpFileStream(SftpSessionMock.Object,
+                                         _path,
+                                         FileMode.Open,
+                                         FileAccess.Read,
+                                         (int)_bufferSize);
+        }
+
+        protected override void Act()
+        {
+            _actual = _target.Read(_buffer, 0, _numberOfBytesToRead);
+        }
+
+        [TestMethod]
+        public void ReadShouldHaveReturnedTheNumberOfBytesRequested()
+        {
+            Assert.AreEqual(_numberOfBytesToRead, _actual);
+        }
+
+        [TestMethod]
+        public void ReadShouldHaveWrittenBytesToTheCallerSuppliedBuffer()
+        {
+            Assert.IsTrue(_serverData1.IsEqualTo(_buffer.Take(_serverData1Length)));
+
+            var bytesWrittenFromSecondRead = _numberOfBytesToRead - _serverData1Length;
+            Assert.IsTrue(_serverData2.Take(bytesWrittenFromSecondRead).IsEqualTo(_buffer.Take(_serverData1Length, bytesWrittenFromSecondRead)));
+        }
+
+        [TestMethod]
+        public void PositionShouldReturnNumberOfBytesWrittenToCallerProvidedBuffer()
+        {
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+
+            Assert.AreEqual(_actual, _target.Position);
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public void ReadShouldReturnAllRemaningBytesFromReadBufferWhenCountIsEqualToNumberOfRemainingBytes()
+        {
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+
+            var numberOfBytesRemainingInReadBuffer = _serverData1Length + _serverData2Length - _numberOfBytesToRead;
+
+            _buffer = new byte[numberOfBytesRemainingInReadBuffer];
+
+            var actual = _target.Read(_buffer, 0, _buffer.Length);
+
+            Assert.AreEqual(_buffer.Length, actual);
+            Assert.IsTrue(_serverData2.Take(_numberOfBytesToRead - _serverData1Length, _buffer.Length).IsEqualTo(_buffer));
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public void ReadShouldReturnAllRemaningBytesFromReadBufferAndReadAgainWhenCountIsGreaterThanNumberOfRemainingBytesAndNewReadReturnsZeroBytes()
+        {
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.RequestRead(_handle, (ulong)(_serverData1Length + _serverData2Length), _readBufferSize)).Returns(Array<byte>.Empty);
+
+            var numberOfBytesRemainingInReadBuffer = _serverData1Length + _serverData2Length - _numberOfBytesToRead;
+
+            _buffer = new byte[numberOfBytesRemainingInReadBuffer + 1];
+
+            var actual = _target.Read(_buffer, 0, _buffer.Length);
+
+            Assert.AreEqual(numberOfBytesRemainingInReadBuffer, actual);
+            Assert.IsTrue(_serverData2.Take(_numberOfBytesToRead - _serverData1Length, numberOfBytesRemainingInReadBuffer).IsEqualTo(_buffer.Take(numberOfBytesRemainingInReadBuffer)));
+            Assert.AreEqual(0, _buffer[numberOfBytesRemainingInReadBuffer]);
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+            SftpSessionMock.Verify(p => p.RequestRead(_handle, (ulong)(_serverData1Length + _serverData2Length), _readBufferSize));
+        }
+    }
+}

+ 148 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndLessThanBufferSize.cs

@@ -0,0 +1,148 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+using Renci.SshNet.Sftp;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndLessThanBufferSize : SftpFileStreamTestBase
+    {
+        private string _path;
+        private SftpFileStream _target;
+        private byte[] _handle;
+        private uint _bufferSize;
+        private uint _readBufferSize;
+        private uint _writeBufferSize;
+        private int _actual;
+        private byte[] _buffer;
+        private byte[] _serverData;
+        private int _serverDataLength;
+        private int _numberOfBytesToRead;
+        private byte[] _originalBuffer;
+
+        protected override void SetupData()
+        {
+            base.SetupData();
+
+            var random = new Random();
+            _path = random.Next().ToString();
+            _handle = GenerateRandom(5, random);
+            _bufferSize = (uint)random.Next(1, 1000);
+            _readBufferSize = 20;
+            _writeBufferSize = 500;
+
+            _numberOfBytesToRead = (int) _readBufferSize + 2; // greater than read buffer size
+            _originalBuffer = GenerateRandom(_numberOfBytesToRead, random);
+            _buffer = _originalBuffer.Copy();
+
+            _serverDataLength = (int) _readBufferSize - 1; // less than read buffer size
+            _serverData = GenerateRandom(_serverDataLength, random);
+
+            Assert.IsTrue(_serverDataLength < _numberOfBytesToRead && _serverDataLength < _readBufferSize);
+        }
+
+        protected override void SetupMocks()
+        {
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.RequestOpen(_path, Flags.Read, false))
+                .Returns(_handle);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.CalculateOptimalReadLength(_bufferSize))
+                .Returns(_readBufferSize);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.CalculateOptimalWriteLength(_bufferSize, _handle))
+                .Returns(_writeBufferSize);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.IsOpen)
+                .Returns(true);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.RequestRead(_handle, 0UL, _readBufferSize))
+                .Returns(_serverData);
+        }
+
+        [TestCleanup]
+        public void TearDown()
+        {
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.RequestClose(_handle));
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _target = new SftpFileStream(SftpSessionMock.Object,
+                                         _path,
+                                         FileMode.Open,
+                                         FileAccess.Read,
+                                         (int)_bufferSize);
+        }
+
+        protected override void Act()
+        {
+            _actual = _target.Read(_buffer, 0, _numberOfBytesToRead);
+        }
+
+        [TestMethod]
+        public void ReadShouldHaveReturnedTheNumberOfBytesReturnedByTheReadFromTheServer()
+        {
+            Assert.AreEqual(_serverDataLength, _actual);
+        }
+
+        [TestMethod]
+        public void ReadShouldHaveWrittenBytesToTheCallerSuppliedBufferAndRemainingBytesShouldRemainUntouched()
+        {
+            Assert.IsTrue(_serverData.IsEqualTo(_buffer.Take(_serverDataLength)));
+            Assert.IsTrue(_originalBuffer.Take(_serverDataLength, _originalBuffer.Length - _serverDataLength).IsEqualTo(_buffer.Take(_serverDataLength, _buffer.Length - _serverDataLength)));
+        }
+
+        [TestMethod]
+        public void PositionShouldReturnNumberOfBytesWrittenToCallerProvidedBuffer()
+        {
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+
+            Assert.AreEqual(_actual, _target.Position);
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public void SubsequentReadShouldReadAgainFromCurrentPositionFromServerAndReturnZeroWhenServerReturnsZeroBytes()
+        {
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.RequestRead(_handle, (ulong) _actual, _readBufferSize))
+                .Returns(Array<byte>.Empty);
+
+            var buffer = _originalBuffer.Copy();
+            var actual = _target.Read(buffer, 0, buffer.Length);
+
+            Assert.AreEqual(0, actual);
+            Assert.IsTrue(_originalBuffer.IsEqualTo(buffer));
+
+            SftpSessionMock.Verify(p => p.RequestRead(_handle, (ulong)_actual, _readBufferSize), Times.Once);
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public void SubsequentReadShouldReadAgainFromCurrentPositionFromServerAndNotUpdatePositionWhenServerReturnsZeroBytes()
+        {
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+            SftpSessionMock.InSequence(MockSequence)
+                .Setup(p => p.RequestRead(_handle, (ulong)_actual, _readBufferSize))
+                .Returns(Array<byte>.Empty);
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+
+            _target.Read(new byte[10], 0, 10);
+
+            Assert.AreEqual(_actual, _target.Position);
+
+            SftpSessionMock.Verify(p => p.RequestRead(_handle, (ulong)_actual, _readBufferSize), Times.Once);
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(3));
+        }
+    }
+}

+ 33 - 14
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesThanCount.cs → src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesFromServerThanCount.cs

@@ -3,11 +3,12 @@ using System.IO;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
 using Renci.SshNet.Sftp;
+using Renci.SshNet.Common;
 
 namespace Renci.SshNet.Tests.Classes.Sftp
 {
     [TestClass]
-    public class SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesThanCount : SftpFileStreamTestBase
+    public class SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesFromServerThanCount : SftpFileStreamTestBase
     {
         private string _path;
         private SftpFileStream _target;
@@ -18,7 +19,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private int _actual;
         private byte[] _buffer;
         private byte[] _serverData;
-        private int _numberOfBytesInReadBuffer;
+        private int _numberOfBytesToWriteToReadBuffer;
         private int _numberOfBytesToRead;
 
         protected override void SetupData()
@@ -34,8 +35,8 @@ namespace Renci.SshNet.Tests.Classes.Sftp
 
             _numberOfBytesToRead = 20;
             _buffer = new byte[_numberOfBytesToRead];
-            _numberOfBytesInReadBuffer = 10;
-            _serverData = GenerateRandom(_buffer.Length + _numberOfBytesInReadBuffer, random);
+            _numberOfBytesToWriteToReadBuffer = 10; // should be less than _readBufferSize
+            _serverData = GenerateRandom(_numberOfBytesToRead + _numberOfBytesToWriteToReadBuffer, random);
         }
 
         protected override void SetupMocks()
@@ -81,40 +82,58 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         }
 
         [TestMethod]
-        public void ReadShouldHaveReturnedTheNumberOfBytesWrittenToBuffer()
+        public void ReadShouldHaveReturnedTheNumberOfBytesWrittenToCallerSuppliedBuffer()
         {
-            Assert.AreEqual(_buffer.Length, _actual);
+            Assert.AreEqual(_numberOfBytesToRead, _actual);
         }
 
         [TestMethod]
         public void ReadShouldHaveWrittenBytesToTheCallerSuppliedBuffer()
         {
-            Assert.IsTrue(_serverData.Take(_buffer.Length).IsEqualTo(_buffer));
+            Assert.IsTrue(_serverData.Take(_actual).IsEqualTo(_buffer));
         }
 
         [TestMethod]
-        public void PositionShouldReturnNumberOfBytesWrittenToBuffer()
+        public void PositionShouldReturnNumberOfBytesWrittenToCallerProvidedBuffer()
         {
             SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
 
-            Assert.AreEqual(_buffer.Length, _target.Position);
+            Assert.AreEqual(_actual, _target.Position);
 
             SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
         }
 
         [TestMethod]
-        public void ReadShouldReturnAllRemaningBytesFromReadBufferWhenCountIsEqualToNumberOfRemainingBytes()
+        public void SubsequentReadShouldReturnAllRemaningBytesFromReadBufferWhenCountIsEqualToNumberOfRemainingBytes()
         {
             SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
 
-            _buffer = new byte[_numberOfBytesInReadBuffer];
+            var buffer = new byte[_numberOfBytesToWriteToReadBuffer];
 
-            var actual = _target.Read(_buffer, 0, _numberOfBytesInReadBuffer);
+            var actual = _target.Read(buffer, 0, _numberOfBytesToWriteToReadBuffer);
 
-            Assert.AreEqual(_numberOfBytesInReadBuffer, actual);
-            Assert.IsTrue(_serverData.Take(_numberOfBytesToRead, _numberOfBytesInReadBuffer).IsEqualTo(_buffer));
+            Assert.AreEqual(_numberOfBytesToWriteToReadBuffer, actual);
+            Assert.IsTrue(_serverData.Take(_numberOfBytesToRead, _numberOfBytesToWriteToReadBuffer).IsEqualTo(buffer));
 
             SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
         }
+
+        [TestMethod]
+        public void SubsequentReadShouldReturnAllRemaningBytesFromReadBufferAndReadAgainWhenCountIsGreaterThanNumberOfRemainingBytesAndNewReadReturnsZeroBytes()
+        {
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.RequestRead(_handle, (ulong)(_serverData.Length), _readBufferSize)).Returns(Array<byte>.Empty);
+
+            var buffer = new byte[_numberOfBytesToWriteToReadBuffer + 1];
+
+            var actual = _target.Read(buffer, 0, buffer.Length);
+
+            Assert.AreEqual(_numberOfBytesToWriteToReadBuffer, actual);
+            Assert.IsTrue(_serverData.Take(_numberOfBytesToRead, _numberOfBytesToWriteToReadBuffer).IsEqualTo(buffer.Take(_numberOfBytesToWriteToReadBuffer)));
+            Assert.AreEqual(0, buffer[_numberOfBytesToWriteToReadBuffer]);
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+            SftpSessionMock.Verify(p => p.RequestRead(_handle, (ulong)(_serverData.Length), _readBufferSize));
+        }
     }
 }

+ 97 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetNegative.cs

@@ -0,0 +1,97 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Sftp;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetNegative : SftpFileStreamTestBase
+    {
+        private Random _random;
+        private string _path;
+        private FileMode _fileMode;
+        private FileAccess _fileAccess;
+        private int _bufferSize;
+        private uint _readBufferSize;
+        private uint _writeBufferSize;
+        private byte[] _handle;
+        private SftpFileStream _target;
+        private int _offset;
+        private EndOfStreamException _actualException;
+
+        protected override void SetupData()
+        {
+            base.SetupData();
+
+            _random = new Random();
+            _path = _random.Next().ToString();
+            _fileMode = FileMode.OpenOrCreate;
+            _fileAccess = FileAccess.Read;
+            _bufferSize = _random.Next(5, 1000);
+            _readBufferSize = (uint)_random.Next(5, 1000);
+            _writeBufferSize = (uint)_random.Next(5, 1000);
+            _handle = GenerateRandom(_random.Next(1, 10), _random);
+            _offset = _random.Next(int.MinValue, -1);
+        }
+
+        protected override void SetupMocks()
+        {
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.RequestOpen(_path, Flags.Read | Flags.CreateNewOrOpen, false))
+                           .Returns(_handle);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.CalculateOptimalReadLength((uint)_bufferSize))
+                           .Returns(_readBufferSize);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.CalculateOptimalWriteLength((uint)_bufferSize, _handle))
+                           .Returns(_writeBufferSize);
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _target = new SftpFileStream(SftpSessionMock.Object, _path, _fileMode, _fileAccess, _bufferSize);
+        }
+
+        protected override void Act()
+        {
+            try
+            {
+                _target.Seek(_offset, SeekOrigin.Begin);
+                Assert.Fail();
+            }
+            catch (EndOfStreamException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void SeekShouldHaveThrownEndOfStreamException()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.IsNull(_actualException.InnerException);
+            Assert.AreEqual("Attempted to read past the end of the stream.", _actualException.Message);
+        }
+
+        [TestMethod]
+        public void IsOpenOnSftpSessionShouldHaveBeenInvokedOnce()
+        {
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
+        }
+
+        [TestMethod]
+        public void PositionShouldReturnZero()
+        {
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+
+            Assert.AreEqual(0L, _target.Position);
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+        }
+    }
+}

+ 88 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetPositive.cs

@@ -0,0 +1,88 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Sftp;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    public class SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetPositive : SftpFileStreamTestBase
+    {
+        private Random _random;
+        private string _path;
+        private FileMode _fileMode;
+        private FileAccess _fileAccess;
+        private int _bufferSize;
+        private uint _readBufferSize;
+        private uint _writeBufferSize;
+        private byte[] _handle;
+        private SftpFileStream _target;
+        private int _offset;
+        private EndOfStreamException _actualException;
+        private long _actual;
+
+        protected override void SetupData()
+        {
+            base.SetupData();
+
+            _random = new Random();
+            _path = _random.Next().ToString();
+            _fileMode = FileMode.OpenOrCreate;
+            _fileAccess = FileAccess.Read;
+            _bufferSize = _random.Next(5, 1000);
+            _readBufferSize = (uint)_random.Next(5, 1000);
+            _writeBufferSize = (uint)_random.Next(5, 1000);
+            _handle = GenerateRandom(_random.Next(1, 10), _random);
+            _offset = _random.Next(1, int.MaxValue);
+        }
+
+        protected override void SetupMocks()
+        {
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.RequestOpen(_path, Flags.Read | Flags.CreateNewOrOpen, false))
+                           .Returns(_handle);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.CalculateOptimalReadLength((uint)_bufferSize))
+                           .Returns(_readBufferSize);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.CalculateOptimalWriteLength((uint)_bufferSize, _handle))
+                           .Returns(_writeBufferSize);
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _target = new SftpFileStream(SftpSessionMock.Object, _path, _fileMode, _fileAccess, _bufferSize);
+        }
+
+        protected override void Act()
+        {
+            _actual = _target.Seek(_offset, SeekOrigin.Begin);
+        }
+
+        [TestMethod]
+        public void SeekShouldHaveReturnedOffset()
+        {
+            Assert.AreEqual(_offset, _actual);
+        }
+
+        [TestMethod]
+        public void IsOpenOnSftpSessionShouldHaveBeenInvokedOnce()
+        {
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
+        }
+
+        [TestMethod]
+        public void PositionShouldReturnOffset()
+        {
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+
+            Assert.AreEqual(_offset, _target.Position);
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+        }
+    }
+}

+ 16 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetZero.cs

@@ -65,5 +65,21 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         {
             Assert.AreEqual(0L, _actual);
         }
+
+        [TestMethod]
+        public void IsOpenOnSftpSessionShouldHaveBeenInvokedOnce()
+        {
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Once);
+        }
+
+        [TestMethod]
+        public void PositionShouldReturnZero()
+        {
+            SftpSessionMock.InSequence(MockSequence).Setup(p => p.IsOpen).Returns(true);
+
+            Assert.AreEqual(0L, _target.Position);
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+        }
     }
 }

+ 19 - 7
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_WithinReadBuffer.cs → src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_NoBuffering.cs

@@ -7,8 +7,8 @@ using Renci.SshNet.Sftp;
 namespace Renci.SshNet.Tests.Classes.Sftp
 {
     [TestClass]
-    [Ignore]
-    public class SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_WithinReadBuffer : SftpFileStreamTestBase
+    //[Ignore]
+    public class SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_NoBuffering : SftpFileStreamTestBase
     {
         private Random _random;
         private string _path;
@@ -35,8 +35,8 @@ namespace Renci.SshNet.Tests.Classes.Sftp
             _readBufferSize = 20;
             _writeBufferSize = (uint) _random.Next(5, 1000);
             _handle = GenerateRandom(_random.Next(1, 10), _random);
-            _buffer = new byte[_readBufferSize - 5];
-            _serverData = GenerateRandom((int) _readBufferSize, _random);
+            _buffer = new byte[_readBufferSize];
+            _serverData = GenerateRandom(_buffer.Length, _random);
         }
 
         protected override void SetupMocks()
@@ -93,18 +93,30 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         }
 
         [TestMethod]
-        public void ReadUpToReadBufferSizeShouldReturnBytesFromReadBuffer()
+        public void IsOpenOnSftpSessionShouldHaveBeenInvokedTwice()
+        {
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public void ReadShouldReturnReadBytesFromServer()
         {
             SftpSessionMock.InSequence(MockSequence)
                            .Setup(p => p.IsOpen)
                            .Returns(true);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.RequestRead(_handle, 0UL, _readBufferSize))
+                           .Returns(new byte[] { 0x05, 0x04 });
 
-            var buffer = new byte[_readBufferSize];
+            var buffer = new byte[1];
 
             var bytesRead = _target.Read(buffer, 0, buffer.Length);
 
             Assert.AreEqual(buffer.Length, bytesRead);
-            Assert.IsTrue(_serverData.IsEqualTo(buffer));
+            Assert.AreEqual(0x05, buffer[0]);
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(3));
+            SftpSessionMock.Verify(p => p.RequestRead(_handle, 0UL, _readBufferSize), Times.Exactly(2));
         }
     }
 }

+ 139 - 0
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_ReadBuffer.cs

@@ -0,0 +1,139 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Sftp;
+
+namespace Renci.SshNet.Tests.Classes.Sftp
+{
+    [TestClass]
+    //[Ignore]
+    public class SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_ReadBuffer : SftpFileStreamTestBase
+    {
+        private Random _random;
+        private string _path;
+        private FileMode _fileMode;
+        private FileAccess _fileAccess;
+        private int _bufferSize;
+        private uint _readBufferSize;
+        private uint _writeBufferSize;
+        private byte[] _handle;
+        private SftpFileStream _target;
+        private long _actual;
+        private byte[] _buffer;
+        private byte[] _serverData1;
+        private byte[] _serverData2;
+
+        protected override void SetupData()
+        {
+            base.SetupData();
+
+            _random = new Random();
+            _path = _random.Next().ToString();
+            _fileMode = FileMode.OpenOrCreate;
+            _fileAccess = FileAccess.Read;
+            _bufferSize = _random.Next(5, 1000);
+            _readBufferSize = 20;
+            _writeBufferSize = (uint) _random.Next(5, 1000);
+            _handle = GenerateRandom(_random.Next(1, 10), _random);
+            _buffer = new byte[2]; // should be less than size of read buffer
+            _serverData1 = GenerateRandom((int) _readBufferSize, _random);
+            _serverData2 = GenerateRandom((int) _readBufferSize, _random);
+        }
+
+        protected override void SetupMocks()
+        {
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.RequestOpen(_path, Flags.Read | Flags.CreateNewOrOpen, false))
+                           .Returns(_handle);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.CalculateOptimalReadLength((uint) _bufferSize))
+                           .Returns(_readBufferSize);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.CalculateOptimalWriteLength((uint) _bufferSize, _handle))
+                           .Returns(_writeBufferSize);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.IsOpen)
+                           .Returns(true);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.RequestRead(_handle, 0UL, _readBufferSize))
+                           .Returns(_serverData1);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.IsOpen)
+                           .Returns(true);
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _target = new SftpFileStream(SftpSessionMock.Object, _path, _fileMode, _fileAccess, _bufferSize);
+            _target.Read(_buffer, 0, _buffer.Length);
+        }
+
+        protected override void Act()
+        {
+            _actual = _target.Seek(0L, SeekOrigin.Begin);
+        }
+
+        [TestMethod]
+        public void SeekShouldHaveReturnedZero()
+        {
+            Assert.AreEqual(0L, _actual);
+        }
+
+        [TestMethod]
+        public void PositionShouldReturnZero()
+        {
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.IsOpen)
+                           .Returns(true);
+
+            Assert.AreEqual(0L, _target.Position);
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(3));
+        }
+
+        [TestMethod]
+        public void IsOpenOnSftpSessionShouldHaveBeenInvokedTwice()
+        {
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public void ReadBytesThatWereNotBufferedBeforeSeekShouldReadBytesFromServer()
+        {
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.IsOpen)
+                           .Returns(true);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.RequestRead(_handle, 0UL, _readBufferSize))
+                           .Returns(_serverData2);
+
+            var bytesRead = _target.Read(_buffer, 0, _buffer.Length);
+
+            Assert.AreEqual(_buffer.Length, bytesRead);
+            Assert.IsTrue(_serverData2.Take(_buffer.Length).IsEqualTo(_buffer));
+
+            SftpSessionMock.Verify(p => p.IsOpen, Times.Exactly(3));
+            SftpSessionMock.Verify(p => p.RequestRead(_handle, 0UL, _readBufferSize), Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public void ReadBytesThatWereBufferedBeforeSeekShouldReadBytesFromServer()
+        {
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.IsOpen)
+                           .Returns(true);
+            SftpSessionMock.InSequence(MockSequence)
+                           .Setup(p => p.RequestRead(_handle, 0UL, _readBufferSize))
+                           .Returns(_serverData2);
+
+            var buffer = new byte[_buffer.Length + 1]; // we read one byte that was previously buffered
+            var bytesRead = _target.Read(buffer, 0, buffer.Length);
+
+            Assert.AreEqual(buffer.Length, bytesRead);
+            Assert.IsTrue(_serverData2.Take(buffer.Length).IsEqualTo(buffer));
+        }
+    }
+}

+ 8 - 0
src/Renci.SshNet.Tests/Common/Extensions.cs

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using Renci.SshNet.Common;
+using System;
 
 namespace Renci.SshNet.Tests.Common
 {
@@ -16,5 +17,12 @@ namespace Renci.SshNet.Tests.Common
 
             return reportedExceptions;
         }
+
+        public static byte[] Copy(this byte[] buffer)
+        {
+            var copy = new byte[buffer.Length];
+            Buffer.BlockCopy(buffer, 0, copy, 0, buffer.Length);
+            return copy;
+        }
     }
 }

+ 7 - 2
src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj

@@ -434,9 +434,14 @@
     <Compile Include="Classes\Sftp\SftpFileStreamTest_Dispose_Disposed.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_Finalize_SessionOpen.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_ReadByte_ReadMode_NoDataInWriteBufferAndNoDataInReadBuffer_LessDataThanReadBufferSizeAvailable.cs" />
-    <Compile Include="Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesThanCount.cs" />
+    <Compile Include="Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndLessThanBufferSize.cs" />
+    <Compile Include="Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndEqualToBufferSize.cs" />
+    <Compile Include="Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesFromServerThanCount.cs" />
+    <Compile Include="Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetNegative.cs" />
+    <Compile Include="Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetPositive.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetZero.cs" />
-    <Compile Include="Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_WithinReadBuffer.cs" />
+    <Compile Include="Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_NoBuffering.cs" />
+    <Compile Include="Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_ReadBuffer.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_SetLength_Closed.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_SetLength_Disposed.cs" />
     <Compile Include="Classes\Sftp\SftpFileStreamTest_SetLength_SessionNotOpen.cs" />

+ 105 - 58
src/Renci.SshNet/Sftp/SftpFileStream.cs

@@ -18,9 +18,9 @@ namespace Renci.SshNet.Sftp
 
         // Buffer information.
         private readonly int _readBufferSize;
-        private readonly byte[] _readBuffer;
+        private byte[] _readBuffer;
         private readonly int _writeBufferSize;
-        private readonly byte[] _writeBuffer;
+        private byte[] _writeBuffer;
         private int _bufferPosition;
         private int _bufferLen;
         private long _position;
@@ -28,7 +28,6 @@ namespace Renci.SshNet.Sftp
         private bool _canRead;
         private bool _canSeek;
         private bool _canWrite;
-        private ulong _serverFilePosition;
 
         private readonly object _lock = new object();
 
@@ -254,15 +253,12 @@ namespace Renci.SshNet.Sftp
             // or SSH_FXP_WRITE message
 
             _readBufferSize = (int) session.CalculateOptimalReadLength((uint) bufferSize);
-            _readBuffer = new byte[_readBufferSize];
             _writeBufferSize = (int) session.CalculateOptimalWriteLength((uint) bufferSize, _handle);
-            _writeBuffer = new byte[_writeBufferSize];
 
             if (mode == FileMode.Append)
             {
                 var attributes = _session.RequestFStat(_handle, false);
                 _position = attributes.Size;
-                _serverFilePosition = (ulong) attributes.Size;
             }
         }
 
@@ -314,6 +310,22 @@ namespace Renci.SshNet.Sftp
         /// <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)
         {
             var readLen = 0;
@@ -342,60 +354,82 @@ namespace Renci.SshNet.Sftp
                     var bytesAvailableInBuffer = _bufferLen - _bufferPosition;
                     if (bytesAvailableInBuffer <= 0)
                     {
-                        _bufferPosition = 0;
-                        _bufferLen = 0;
-
                         var data = _session.RequestRead(_handle, (ulong) _position, (uint) _readBufferSize);
 
-                        // TODO: don't we need to take into account the number of bytes read (data.Length) ?
-                        _serverFilePosition = (ulong) _position;
-
                         if (data.Length == 0)
                         {
+                            _bufferPosition = 0;
+                            _bufferLen = 0;
+
                             break;
                         }
 
-                        // determine number of bytes that we can read into caller-provided buffer
-                        var bytesToWriteToCallerBuffer = Math.Min(data.Length, count);
+                        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);
-                        // advance offset to start writing bytes into caller-provided buffer
-                        offset += bytesToWriteToCallerBuffer;
-                        // update number of bytes left to read
-                        count -= bytesToWriteToCallerBuffer;
-                        // record total number of bytes read into caller-provided buffer
-                        readLen += bytesToWriteToCallerBuffer;
                         // update stream position
                         _position += bytesToWriteToCallerBuffer;
+                        // record total number of bytes read into caller-provided buffer
+                        readLen += bytesToWriteToCallerBuffer;
 
-                        if (data.Length > bytesToWriteToCallerBuffer)
+                        // 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)
                         {
-                            // copy remaining bytes to read buffer
-                            _bufferLen = data.Length - bytesToWriteToCallerBuffer;
-                            Buffer.BlockCopy(data, bytesToWriteToCallerBuffer, _readBuffer, 0, _bufferLen);
+                            break;
                         }
+
+                        // advance offset to start writing bytes into caller-provided buffer
+                        offset += bytesToWriteToCallerBuffer;
+                        // update number of bytes left to read into caller-provided buffer
+                        count -= bytesToWriteToCallerBuffer;
                     }
                     else
                     {
-                        // determine number of bytes that we can write from read buffer to caller-provided buffer
-                        var bytesToWriteToCallerBuffer = Math.Min(bytesAvailableInBuffer, count);
+                        // 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(_readBuffer, _bufferPosition, buffer, offset, bytesToWriteToCallerBuffer);
+                        Buffer.BlockCopy(GetOrCreateReadBuffer(), _bufferPosition, buffer, offset, bytesAvailableInBuffer);
                         // update position in read buffer
-                        _bufferPosition += bytesToWriteToCallerBuffer;
+                        _bufferPosition += bytesAvailableInBuffer;
+                        // update stream position
+                        _position += bytesAvailableInBuffer;
+                        // record total number of bytes read into caller-provided buffer
+                        readLen += bytesAvailableInBuffer;
                         // advance offset to start writing bytes into caller-provided buffer
                         offset += bytesAvailableInBuffer;
                         // update number of bytes left to read
-                        count -= bytesToWriteToCallerBuffer;
-                        // record total number of bytes read into caller-provided buffer
-                        readLen += bytesToWriteToCallerBuffer;
-                        // update stream position
-                        _position += bytesToWriteToCallerBuffer;
+                        count -= bytesAvailableInBuffer;
                     }
                 }
             }
 
-            // Return the number of bytes that were read to the caller.
+            // return the number of bytes that were read to the caller.
             return readLen;
         }
 
@@ -418,28 +452,32 @@ namespace Renci.SshNet.Sftp
                 // Setup the object for reading.
                 SetupRead();
 
+                byte[] readBuffer;
+
                 // Read more data into the internal buffer if necessary.
                 if (_bufferPosition >= _bufferLen)
                 {
-                    _bufferPosition = 0;
-
                     var data = _session.RequestRead(_handle, (ulong) _position, (uint) _readBufferSize);
-
-                    _bufferLen = data.Length;
-                    _serverFilePosition = (ulong) _position;
-
-                    if (_bufferLen == 0)
+                    if (data.Length == 0)
                     {
                         // We've reached EOF.
                         return -1;
                     }
 
-                    Buffer.BlockCopy(data, 0, _readBuffer, 0, _bufferLen);
+                    readBuffer = GetOrCreateReadBuffer();
+                    Buffer.BlockCopy(data, 0, readBuffer, 0, data.Length);
+
+                    _bufferPosition = 0;
+                    _bufferLen = data.Length;
+                }
+                else
+                {
+                    readBuffer = GetOrCreateReadBuffer();
                 }
 
                 // Extract the next byte from the buffer.
                 ++_position;
-                return _readBuffer[_bufferPosition++];
+                return readBuffer[_bufferPosition++];
             }
         }
 
@@ -501,7 +539,6 @@ namespace Renci.SshNet.Sftp
                         throw new EndOfStreamException("End of stream.");
                     }
                     _position = newPosn;
-                    _serverFilePosition = (ulong)newPosn;
                 }
                 else
                 {
@@ -510,8 +547,7 @@ namespace Renci.SshNet.Sftp
                     if (origin == SeekOrigin.Begin)
                     {
                         newPosn = _position - _bufferPosition;
-                        if (offset >= newPosn && offset <
-                                (newPosn + _bufferLen))
+                        if (offset >= newPosn && offset < (newPosn + _bufferLen))
                         {
                             _bufferPosition = (int)(offset - newPosn);
                             _position = offset;
@@ -524,8 +560,7 @@ namespace Renci.SshNet.Sftp
                         if (newPosn >= (_position - _bufferPosition) &&
                            newPosn < (_position - _bufferPosition + _bufferLen))
                         {
-                            _bufferPosition =
-                                (int)(newPosn - (_position - _bufferPosition));
+                            _bufferPosition = (int) (newPosn - (_position - _bufferPosition));
                             _position = newPosn;
                             return _position;
                         }
@@ -645,14 +680,13 @@ namespace Renci.SshNet.Sftp
                     {
                         using (var wait = new AutoResetEvent(false))
                         {
-                            _session.RequestWrite(_handle, _serverFilePosition, buffer, offset, tempLen, wait);
-                            _serverFilePosition += (ulong) tempLen;
+                            _session.RequestWrite(_handle, (ulong) _position, buffer, offset, tempLen, wait);
                         }
                     }
                     else
                     {
                         // No: copy the data to the write buffer first.
-                        Buffer.BlockCopy(buffer, offset, _writeBuffer, _bufferPosition, tempLen);
+                        Buffer.BlockCopy(buffer, offset, GetOrCreateWriteBuffer(), _bufferPosition, tempLen);
                         _bufferPosition += tempLen;
                     }
 
@@ -668,8 +702,7 @@ namespace Renci.SshNet.Sftp
                 {
                     using (var wait = new AutoResetEvent(false))
                     {
-                        _session.RequestWrite(_handle, _serverFilePosition, _writeBuffer, 0, _bufferPosition, wait);
-                        _serverFilePosition += (ulong) _bufferPosition;
+                        _session.RequestWrite(_handle, (ulong) (_position - _bufferPosition), GetOrCreateWriteBuffer(), 0, _bufferPosition, wait);
                     }
 
                     _bufferPosition = 0;
@@ -694,20 +727,21 @@ namespace Renci.SshNet.Sftp
                 // Setup the object for writing.
                 SetupWrite();
 
+                var writeBuffer = GetOrCreateWriteBuffer();
+
                 // Flush the current buffer if it is full.
                 if (_bufferPosition >= _writeBufferSize)
                 {
                     using (var wait = new AutoResetEvent(false))
                     {
-                        _session.RequestWrite(_handle, _serverFilePosition, _writeBuffer, 0, _bufferPosition, wait);
-                        _serverFilePosition += (ulong) _bufferPosition;
+                        _session.RequestWrite(_handle, (ulong) (_position - _bufferPosition), writeBuffer, 0, _bufferPosition, wait);
                     }
 
                     _bufferPosition = 0;
                 }
 
                 // Write the byte into the buffer and advance the posn.
-                _writeBuffer[_bufferPosition++] = value;
+                writeBuffer[_bufferPosition++] = value;
                 ++_position;
             }
         }
@@ -754,6 +788,20 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+        private byte[] GetOrCreateReadBuffer()
+        {
+            if (_readBuffer == null)
+                _readBuffer = new byte[_readBufferSize];
+            return _readBuffer;
+        }
+
+        private byte[] GetOrCreateWriteBuffer()
+        {
+            if (_writeBuffer == null)
+                _writeBuffer = new byte[_writeBufferSize];
+            return _writeBuffer;
+        }
+
         /// <summary>
         /// Flushes the read data from the buffer.
         /// </summary>
@@ -779,8 +827,7 @@ namespace Renci.SshNet.Sftp
             {
                 using (var wait = new AutoResetEvent(false))
                 {
-                    _session.RequestWrite(_handle, _serverFilePosition, _writeBuffer, 0, _bufferPosition, wait);
-                    _serverFilePosition += (ulong) _bufferPosition;
+                    _session.RequestWrite(_handle, (ulong) (_position - _bufferPosition), _writeBuffer, 0, _bufferPosition, wait);
                 }
 
                 _bufferPosition = 0;

+ 6 - 1
src/Renci.SshNet/SftpClient.cs

@@ -1,5 +1,4 @@
 using System;
-
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
@@ -1425,6 +1424,9 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public string[] ReadAllLines(string path, Encoding encoding)
         {
+            // we use the default buffer size for StreamReader - which is 1024 bytes - and the configured buffer size
+            // for the SftpFileStream; may want to revisit this later
+
             var lines = new List<string>();
             using (var stream = new StreamReader(OpenRead(path), encoding))
             {
@@ -1464,6 +1466,9 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public string ReadAllText(string path, Encoding encoding)
         {
+            // we use the default buffer size for StreamReader - which is 1024 bytes - and the configured buffer size
+            // for the SftpFileStream; may want to revisit this later
+
             using (var stream = new StreamReader(OpenRead(path), encoding))
             {
                 return stream.ReadToEnd();