diff --git a/src/Renci.SshNet.NETCore/Renci.SshNet.NETCore.csproj b/src/Renci.SshNet.NETCore/Renci.SshNet.NETCore.csproj index c06afc025..6f2178443 100644 --- a/src/Renci.SshNet.NETCore/Renci.SshNet.NETCore.csproj +++ b/src/Renci.SshNet.NETCore/Renci.SshNet.NETCore.csproj @@ -6,6 +6,7 @@ true Renci.SshNet ../Renci.SshNet.snk + 5 true true false diff --git a/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj b/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj index 2c350023e..928789069 100644 --- a/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj +++ b/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj @@ -1202,8 +1202,29 @@ Classes\Sftp\SftpFileStreamTest_ReadByte_ReadMode_NoDataInWriteBufferAndNoDataInReadBuffer_LessDataThanReadBufferSizeAvailable.cs - - Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesThanCount.cs + + Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndEqualToBufferSize.cs + + + Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndLessThanBufferSize.cs + + + Classes\Sftp\SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesFromServerThanCount.cs + + + Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetNegative.cs + + + Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetPositive.cs + + + Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetZero.cs + + + Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_NoBuffering.cs + + + Classes\Sftp\SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_ReadBuffer.cs Classes\Sftp\SftpFileStreamTest_SetLength_Closed.cs @@ -1223,6 +1244,9 @@ Classes\Sftp\SftpFileStreamTest_SetLength_SessionOpen_FIleAccessWrite.cs + + Classes\Sftp\SftpFileStreamTest_Write_SessionOpen_CountGreatherThanTwoTimesTheWriteBufferSize.cs + Classes\Sftp\SftpFileSystemInformationTest.cs diff --git a/src/Renci.SshNet.Tests/Classes/BaseClientTest_Connected_KeepAlivesNotSentConcurrently.cs b/src/Renci.SshNet.Tests/Classes/BaseClientTest_Connected_KeepAlivesNotSentConcurrently.cs index ff6038744..262668d7e 100644 --- a/src/Renci.SshNet.Tests/Classes/BaseClientTest_Connected_KeepAlivesNotSentConcurrently.cs +++ b/src/Renci.SshNet.Tests/Classes/BaseClientTest_Connected_KeepAlivesNotSentConcurrently.cs @@ -11,8 +11,10 @@ public class BaseClientTest_Connected_KeepAlivesNotSentConcurrently { private Mock _serviceFactoryMock; private Mock _sessionMock; + private MockSequence _mockSequence; private BaseClient _client; private ConnectionInfo _connectionInfo; + private ManualResetEvent _keepAliveSent; [TestInitialize] public void Setup() @@ -24,20 +26,33 @@ public void Setup() [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(MockBehavior.Strict); _sessionMock = new Mock(MockBehavior.Strict); - _serviceFactoryMock.Setup(p => p.CreateSession(_connectionInfo)).Returns(_sessionMock.Object); - _sessionMock.Setup(p => p.Connect()); - _sessionMock.Setup(p => p.TrySendMessage(It.IsAny())) + _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())) .Returns(true) - .Callback(() => Thread.Sleep(300)); + .Callback(() => + { + Thread.Sleep(300); + _keepAliveSent.Set(); + }); _client = new MyClient(_connectionInfo, false, _serviceFactoryMock.Object) { @@ -51,6 +66,12 @@ protected void Act() // 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] diff --git a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndEqualToBufferSize.cs b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndEqualToBufferSize.cs new file mode 100644 index 000000000..5f6f24be9 --- /dev/null +++ b/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.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)); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndLessThanBufferSize.cs b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadLessBytesFromServerThanCountAndLessThanBufferSize.cs new file mode 100644 index 000000000..85a6679b8 --- /dev/null +++ b/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.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.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)); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesThanCount.cs b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesFromServerThanCount.cs similarity index 60% rename from src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesThanCount.cs rename to src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesFromServerThanCount.cs index d1956e654..97c66422b 100644 --- a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesThanCount.cs +++ b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreBytesFromServerThanCount.cs @@ -3,11 +3,12 @@ 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 @@ public class SftpFileStreamTest_Read_ReadMode_NoDataInReaderBufferAndReadMoreByt 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 @@ protected override void SetupData() _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 @@ protected override void Act() } [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.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)); + } } } diff --git a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetNegative.cs b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetNegative.cs new file mode 100644 index 000000000..712880d85 --- /dev/null +++ b/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)); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetPositive.cs b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetPositive.cs new file mode 100644 index 000000000..c0f368b8c --- /dev/null +++ b/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)); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetZero.cs b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetZero.cs index 98ae83bf8..27e8d96fc 100644 --- a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetZero.cs +++ b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetZero.cs @@ -65,5 +65,21 @@ public void SeekShouldHaveReturnedZero() { 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)); + } } } diff --git a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_WithinReadBuffer.cs b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_NoBuffering.cs similarity index 79% rename from src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_WithinReadBuffer.cs rename to src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_NoBuffering.cs index 50b3172af..29b683375 100644 --- a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_WithinReadBuffer.cs +++ b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_NoBuffering.cs @@ -7,8 +7,8 @@ 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 @@ protected override void SetupData() _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 @@ public void PositionShouldReturnZero() } [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)); } } } diff --git a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_ReadBuffer.cs b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtMiddleOfStream_OriginBeginAndOffsetZero_ReadBuffer.cs new file mode 100644 index 000000000..624a9759f --- /dev/null +++ b/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)); + } + } +} diff --git a/src/Renci.SshNet.Tests/Common/Extensions.cs b/src/Renci.SshNet.Tests/Common/Extensions.cs index 68a2923ff..6bb1e45ea 100644 --- a/src/Renci.SshNet.Tests/Common/Extensions.cs +++ b/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 @@ public static string AsString(this IList exceptionEvents) 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; + } } } diff --git a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj index fa976e89c..9bf0c4cd4 100644 --- a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj +++ b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj @@ -434,9 +434,14 @@ - + + + + + - + + diff --git a/src/Renci.SshNet/Sftp/SftpFileStream.cs b/src/Renci.SshNet/Sftp/SftpFileStream.cs index fbc4d4674..500e1ba9c 100644 --- a/src/Renci.SshNet/Sftp/SftpFileStream.cs +++ b/src/Renci.SshNet/Sftp/SftpFileStream.cs @@ -18,9 +18,9 @@ public class SftpFileStream : Stream // 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 @@ public class SftpFileStream : Stream private bool _canRead; private bool _canSeek; private bool _canWrite; - private ulong _serverFilePosition; private readonly object _lock = new object(); @@ -254,15 +253,12 @@ internal SftpFileStream(ISftpSession session, string path, FileMode mode, FileAc // 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 @@ public override void Flush() /// An I/O error occurs. /// The stream does not support reading. /// Methods were called after the stream was closed. + /// + /// + /// This method attempts to read up to bytes. This either from the buffer, from the + /// server (using one or more SSH_FXP_READ requests) or using a combination of both. + /// + /// + /// The read loop is interrupted when either bytes are read, the server returns zero + /// bytes (EOF) or less bytes than the read buffer size. + /// + /// + /// When a server returns less number of bytes than the read buffer size, this may indicate that EOF has + /// been reached. A subsequent (SSH_FXP_READ) 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 or invocation. + /// + /// public override int Read(byte[] buffer, int offset, int count) { var readLen = 0; @@ -342,60 +354,82 @@ public override int Read(byte[] buffer, int offset, int count) 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 @@ public override int ReadByte() // 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 @@ public override long Seek(long offset, SeekOrigin origin) throw new EndOfStreamException("End of stream."); } _position = newPosn; - _serverFilePosition = (ulong)newPosn; } else { @@ -510,8 +547,7 @@ public override long Seek(long offset, SeekOrigin origin) 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 @@ public override long Seek(long offset, SeekOrigin origin) 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 @@ public override void Write(byte[] buffer, int offset, int count) { 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 @@ public override void Write(byte[] buffer, int offset, int count) { 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 @@ public override void WriteByte(byte value) // 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 @@ protected override void Dispose(bool disposing) } } + private byte[] GetOrCreateReadBuffer() + { + if (_readBuffer == null) + _readBuffer = new byte[_readBufferSize]; + return _readBuffer; + } + + private byte[] GetOrCreateWriteBuffer() + { + if (_writeBuffer == null) + _writeBuffer = new byte[_writeBufferSize]; + return _writeBuffer; + } + /// /// Flushes the read data from the buffer. /// @@ -779,8 +827,7 @@ private void FlushWriteBuffer() { 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; diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 59d222bb9..3bde01ad3 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/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 @@ public string[] ReadAllLines(string path) /// The method was called after the client was disposed. 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(); using (var stream = new StreamReader(OpenRead(path), encoding)) { @@ -1464,6 +1466,9 @@ public string ReadAllText(string path) /// The method was called after the client was disposed. 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();