Prechádzať zdrojové kódy

Introduce extensible remote path quoting mechanism

Introduce extensible remote path quoting mechanism.
Fixes issues #256 and #108.
Gert Driesen 8 rokov pred
rodič
commit
cde333185c
40 zmenil súbory, kde vykonal 3971 pridanie a 327 odobranie
  1. 15 0
      src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj
  2. 16 1
      src/Renci.SshNet.Silverlight/Renci.SshNet.Silverlight.csproj
  3. 16 1
      src/Renci.SshNet.Silverlight5/Renci.SshNet.Silverlight5.csproj
  4. 19 1
      src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj
  5. 61 37
      src/Renci.SshNet.Tests/Classes/Channels/ChannelTest_Dispose_SessionIsConnectedAndChannelIsOpen_EofReceived.cs
  6. 136 5
      src/Renci.SshNet.Tests/Classes/Common/CountdownEventTest.cs
  7. 109 0
      src/Renci.SshNet.Tests/Classes/Common/CountdownEventTest_Dispose_NotSet.cs
  8. 107 0
      src/Renci.SshNet.Tests/Classes/Common/CountdownEventTest_Dispose_Set.cs
  9. 9 0
      src/Renci.SshNet.Tests/Classes/Common/ExtensionsTest_ShellQuote.cs
  10. 157 0
      src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_GetFileName.cs
  11. 1128 0
      src/Renci.SshNet.Tests/Classes/RemotePathDoubleQuoteTransformationTest.cs
  12. 1128 0
      src/Renci.SshNet.Tests/Classes/RemotePathShellQuoteTransformationTest.cs
  13. 228 78
      src/Renci.SshNet.Tests/Classes/ScpClientTest.cs
  14. 45 0
      src/Renci.SshNet.Tests/Classes/ScpClientTestBase.cs
  15. 26 26
      src/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse.cs
  16. 25 26
      src/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndFileInfo_SendExecRequestReturnsFalse.cs
  17. 26 26
      src/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndStream_SendExecRequestReturnsFalse.cs
  18. 26 26
      src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse.cs
  19. 27 27
      src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs
  20. 20 34
      src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs
  21. 26 26
      src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse.cs
  22. 0 1
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Flush_WriteMode_NoDataInBuffer.cs
  23. 0 1
      src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Seek_PositionedAtBeginningOfStream_OriginBeginAndOffsetPositive.cs
  24. 6 0
      src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
  25. 15 0
      src/Renci.SshNet.UAP10/Renci.SshNet.UAP10.csproj
  26. 15 0
      src/Renci.SshNet.WindowsPhone/Renci.SshNet.WindowsPhone.csproj
  27. 16 1
      src/Renci.SshNet.WindowsPhone8/Renci.SshNet.WindowsPhone8.csproj
  28. 18 0
      src/Renci.SshNet/Common/CountdownEvent.cs
  29. 14 4
      src/Renci.SshNet/Common/PosixPath.cs
  30. 19 0
      src/Renci.SshNet/IRemotePathTransformation.cs
  31. 11 0
      src/Renci.SshNet/IServiceFactory.cs
  32. 11 0
      src/Renci.SshNet/PasswordAuthenticationMethod.cs
  33. 72 0
      src/Renci.SshNet/RemotePathDoubleQuoteTransformation.cs
  34. 32 0
      src/Renci.SshNet/RemotePathNoneTransformation.cs
  35. 193 0
      src/Renci.SshNet/RemotePathShellQuoteTransformation.cs
  36. 143 0
      src/Renci.SshNet/RemotePathTransformation.cs
  37. 5 0
      src/Renci.SshNet/Renci.SshNet.csproj
  38. 4 4
      src/Renci.SshNet/ScpClient.NET.cs
  39. 33 2
      src/Renci.SshNet/ScpClient.cs
  40. 14 0
      src/Renci.SshNet/ServiceFactory.cs

+ 15 - 0
src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj

@@ -308,6 +308,9 @@
     <Compile Include="..\Renci.SshNet\IForwardedPort.cs">
       <Link>IForwardedPort.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\IRemotePathTransformation.cs">
+      <Link>IRemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\IServiceFactory.cs">
       <Link>IServiceFactory.cs</Link>
     </Compile>
@@ -578,6 +581,18 @@
     <Compile Include="..\Renci.SshNet\ProxyTypes.cs">
       <Link>ProxyTypes.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathDoubleQuoteTransformation.cs">
+      <Link>RemotePathDoubleQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathNoneTransformation.cs">
+      <Link>RemotePathNoneTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathShellQuoteTransformation.cs">
+      <Link>RemotePathShellQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathTransformation.cs">
+      <Link>RemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\ScpClient.cs">
       <Link>ScpClient.cs</Link>
     </Compile>

+ 16 - 1
src/Renci.SshNet.Silverlight/Renci.SshNet.Silverlight.csproj

@@ -314,6 +314,9 @@
     <Compile Include="..\Renci.SshNet\IForwardedPort.cs">
       <Link>IForwardedPort.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\IRemotePathTransformation.cs">
+      <Link>IRemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\IServiceFactory.cs">
       <Link>IServiceFactory.cs</Link>
     </Compile>
@@ -572,6 +575,18 @@
     <Compile Include="..\Renci.SshNet\ProxyTypes.cs">
       <Link>ProxyTypes.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathDoubleQuoteTransformation.cs">
+      <Link>RemotePathDoubleQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathNoneTransformation.cs">
+      <Link>RemotePathNoneTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathShellQuoteTransformation.cs">
+      <Link>RemotePathShellQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathTransformation.cs">
+      <Link>RemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\ScpClient.cs">
       <Link>ScpClient.cs</Link>
     </Compile>
@@ -949,7 +964,7 @@
       <FlavorProperties GUID="{A1591282-1198-4647-A2B1-27E5FF5F6F3B}">
         <SilverlightProjectProperties />
       </FlavorProperties>
-      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
+      <UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 16 - 1
src/Renci.SshNet.Silverlight5/Renci.SshNet.Silverlight5.csproj

@@ -323,6 +323,9 @@
     <Compile Include="..\Renci.SshNet\IForwardedPort.cs">
       <Link>IForwardedPort.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\IRemotePathTransformation.cs">
+      <Link>IRemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\IServiceFactory.cs">
       <Link>IServiceFactory.cs</Link>
     </Compile>
@@ -581,6 +584,18 @@
     <Compile Include="..\Renci.SshNet\ProxyTypes.cs">
       <Link>ProxyTypes.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathDoubleQuoteTransformation.cs">
+      <Link>RemotePathDoubleQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathNoneTransformation.cs">
+      <Link>RemotePathNoneTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathShellQuoteTransformation.cs">
+      <Link>RemotePathShellQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathTransformation.cs">
+      <Link>RemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\ScpClient.cs">
       <Link>ScpClient.cs</Link>
     </Compile>
@@ -955,7 +970,7 @@
       <FlavorProperties GUID="{A1591282-1198-4647-A2B1-27E5FF5F6F3B}">
         <SilverlightProjectProperties />
       </FlavorProperties>
-      <UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
+      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 19 - 1
src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj

@@ -273,6 +273,12 @@
     <Compile Include="..\Renci.SshNet.Tests\Classes\Common\CountdownEventTest.cs">
       <Link>Classes\Common\CountdownEventTest.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Common\CountdownEventTest_Dispose_NotSet.cs">
+      <Link>Classes\Common\CountdownEventTest_Dispose_NotSet.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Common\CountdownEventTest_Dispose_Set.cs">
+      <Link>Classes\Common\CountdownEventTest_Dispose_Set.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\Common\DerDataTest.cs">
       <Link>Classes\Common\DerDataTest.cs</Link>
     </Compile>
@@ -327,6 +333,9 @@
     <Compile Include="..\Renci.SshNet.Tests\Classes\Common\PortForwardEventArgsTest.cs">
       <Link>Classes\Common\PortForwardEventArgsTest.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Common\PosixPathTest_GetFileName.cs">
+      <Link>Classes\Common\PosixPathTest_GetFileName.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\Common\ProxyExceptionTest.cs">
       <Link>Classes\Common\ProxyExceptionTest.cs</Link>
     </Compile>
@@ -765,9 +774,18 @@
     <Compile Include="..\Renci.SshNet.Tests\Classes\PrivateKeyFileTest.cs">
       <Link>Classes\PrivateKeyFileTest.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\RemotePathDoubleQuoteTransformationTest.cs">
+      <Link>Classes\RemotePathDoubleQuoteTransformationTest.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\RemotePathShellQuoteTransformationTest.cs">
+      <Link>Classes\RemotePathShellQuoteTransformationTest.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\ScpClientTest.cs">
       <Link>Classes\ScpClientTest.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ScpClientTestBase.cs">
+      <Link>Classes\ScpClientTestBase.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse.cs">
       <Link>Classes\ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse.cs</Link>
     </Compile>
@@ -1584,7 +1602,7 @@
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <ProjectExtensions>
     <VisualStudio>
-      <UserProperties ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
+      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 61 - 37
src/Renci.SshNet.Tests/Classes/Channels/ChannelTest_Dispose_SessionIsConnectedAndChannelIsOpen_EofReceived.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.Threading;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
@@ -21,9 +20,59 @@ namespace Renci.SshNet.Tests.Classes.Channels
         private uint _remotePacketSize;
         private ChannelStub _channel;
         private List<ChannelEventArgs> _channelClosedRegister;
+        private List<ChannelEventArgs> _channelEndOfDataRegister;
         private IList<ExceptionEventArgs> _channelExceptionRegister;
         private ManualResetEvent _channelClosedReceived;
 
+        private void SetupData()
+        {
+            var random = new Random();
+
+            _localChannelNumber = (uint) random.Next(0, int.MaxValue);
+            _localWindowSize = (uint) random.Next(0, int.MaxValue);
+            _localPacketSize = (uint) random.Next(0, int.MaxValue);
+            _remoteChannelNumber = (uint) random.Next(0, int.MaxValue);
+            _remoteWindowSize = (uint) random.Next(0, int.MaxValue);
+            _remotePacketSize = (uint) random.Next(0, int.MaxValue);
+            _channelClosedRegister = new List<ChannelEventArgs>();
+            _channelEndOfDataRegister = new List<ChannelEventArgs>();
+            _channelExceptionRegister = new List<ExceptionEventArgs>();
+            _channelClosedReceived = new ManualResetEvent(false);
+        }
+
+        private void CreateMocks()
+        {
+            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
+        }
+
+        private void SetupMocks()
+        {
+            var sequence = new MockSequence();
+
+            _sessionMock.InSequence(sequence).Setup(p => p.IsConnected).Returns(true);
+            _sessionMock.InSequence(sequence).Setup(p => p.TrySendMessage(It.Is<ChannelCloseMessage>(c => c.LocalChannelNumber == _remoteChannelNumber))).Returns(true);
+            _sessionMock.InSequence(sequence).Setup(p => p.WaitOnHandle(It.IsAny<EventWaitHandle>()))
+                .Callback<WaitHandle>(w =>
+                {
+                    new Thread(() =>
+                    {
+                        Thread.Sleep(100);
+                        // raise ChannelCloseReceived event to set waithandle for receiving
+                        // SSH_MSG_CHANNEL_CLOSE message from server which is waited on after
+                        // sending the SSH_MSG_CHANNEL_CLOSE message to the server
+                        // 
+                        // we're mocking the wait on the ChannelCloseMessage, but we still want
+                        // to get the channel in the state that it would have after actually receiving
+                        // the ChannelCloseMessage
+                        _sessionMock.Raise(s => s.ChannelCloseReceived += null, new MessageEventArgs<ChannelCloseMessage>(new ChannelCloseMessage(_localChannelNumber)));
+                        // signal that the ChannelCloseMessage was received; we use this to verify whether we've actually
+                        // waited on the EventWaitHandle to be set
+                        _channelClosedReceived.Set();
+                    }).Start();
+                    w.WaitOne();
+                });
+        }
+
         [TestInitialize]
         public void Initialize()
         {
@@ -43,45 +92,13 @@ namespace Renci.SshNet.Tests.Classes.Channels
 
         private void Arrange()
         {
-            var random = new Random();
-            _localChannelNumber = (uint)random.Next(0, int.MaxValue);
-            _localWindowSize = (uint)random.Next(0, int.MaxValue);
-            _localPacketSize = (uint)random.Next(0, int.MaxValue);
-            _remoteChannelNumber = (uint)random.Next(0, int.MaxValue);
-            _remoteWindowSize = (uint)random.Next(0, int.MaxValue);
-            _remotePacketSize = (uint)random.Next(0, int.MaxValue);
-            _channelClosedRegister = new List<ChannelEventArgs>();
-            _channelExceptionRegister = new List<ExceptionEventArgs>();
-            _channelClosedReceived = new ManualResetEvent(false);
-
-            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
-
-            var sequence = new MockSequence();
-            _sessionMock.InSequence(sequence).Setup(p => p.IsConnected).Returns(true);
-            _sessionMock.InSequence(sequence).Setup(p => p.TrySendMessage(It.Is<ChannelCloseMessage>(c => c.LocalChannelNumber == _remoteChannelNumber))).Returns(true);
-            _sessionMock.InSequence(sequence).Setup(p => p.WaitOnHandle(It.IsAny<EventWaitHandle>()))
-                .Callback<WaitHandle>(w =>
-                    {
-                        new Thread(() =>
-                            {
-                                Thread.Sleep(100);
-                                // signal that the ChannelCloseMessage was received; we use this to verify whether we've actually
-                                // waited on the EventWaitHandle to be set
-                                _channelClosedReceived.Set();
-                                // raise ChannelCloseReceived event to set waithandle for receiving
-                                // SSH_MSG_CHANNEL_CLOSE message from server which is waited on after
-                                // sending the SSH_MSG_CHANNEL_CLOSE message to the server
-                                // 
-                                // we're mocking the wait on the ChannelCloseMessage, but we still want
-                                // to get the channel in the state that it would have after actually receiving
-                                // the ChannelCloseMessage
-                                _sessionMock.Raise(s => s.ChannelCloseReceived += null, new MessageEventArgs<ChannelCloseMessage>(new ChannelCloseMessage(_localChannelNumber)));
-                            }).Start();
-                        w.WaitOne();
-                    });
+            SetupData();
+            CreateMocks();
+            SetupMocks();
 
             _channel = new ChannelStub(_sessionMock.Object, _localChannelNumber, _localWindowSize, _localPacketSize);
             _channel.Closed += (sender, args) => _channelClosedRegister.Add(args);
+            _channel.EndOfData += (sender, args) => _channelEndOfDataRegister.Add(args);
             _channel.Exception += (sender, args) => _channelExceptionRegister.Add(args);
             _channel.InitializeRemoteChannelInfo(_remoteChannelNumber, _remoteWindowSize, _remotePacketSize);
             _channel.SetIsOpen(true);
@@ -137,6 +154,13 @@ namespace Renci.SshNet.Tests.Classes.Channels
             Assert.AreEqual(_localChannelNumber, _channelClosedRegister[0].ChannelNumber);
         }
 
+        [TestMethod]
+        public void EndOfDataEventShouldHaveFiredOnce()
+        {
+            Assert.AreEqual(1, _channelEndOfDataRegister.Count);
+            Assert.AreEqual(_localChannelNumber, _channelEndOfDataRegister[0].ChannelNumber);
+        }
+
         [TestMethod]
         public void ExceptionShouldNeverHaveFired()
         {

+ 136 - 5
src/Renci.SshNet.Tests/Classes/Common/CountdownEventTest.cs

@@ -26,6 +26,7 @@ namespace Renci.SshNet.Tests.Classes.Common
             var countdownEvent = CreateCountdownEvent(initialCount);
             Assert.AreEqual(initialCount, countdownEvent.CurrentCount);
             Assert.IsFalse(countdownEvent.IsSet);
+            Assert.IsFalse(countdownEvent.WaitHandle.WaitOne(0));
             countdownEvent.Dispose();
         }
 
@@ -37,6 +38,7 @@ namespace Renci.SshNet.Tests.Classes.Common
             var countdownEvent = CreateCountdownEvent(0);
             Assert.AreEqual(initialCount, countdownEvent.CurrentCount);
             Assert.IsTrue(countdownEvent.IsSet);
+            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
             countdownEvent.Dispose();
         }
 
@@ -49,6 +51,7 @@ namespace Renci.SshNet.Tests.Classes.Common
             Assert.IsFalse(countdownEvent.Signal());
             Assert.AreEqual(--initialCount, countdownEvent.CurrentCount);
             Assert.IsFalse(countdownEvent.IsSet);
+            Assert.IsFalse(countdownEvent.WaitHandle.WaitOne(0));
             countdownEvent.Dispose();
         }
 
@@ -59,6 +62,7 @@ namespace Renci.SshNet.Tests.Classes.Common
             Assert.IsTrue(countdownEvent.Signal());
             Assert.AreEqual(0, countdownEvent.CurrentCount);
             Assert.IsTrue(countdownEvent.IsSet);
+            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
             countdownEvent.Dispose();
         }
 
@@ -82,10 +86,6 @@ namespace Renci.SshNet.Tests.Classes.Common
             }
         }
 
-        public void CurrentCountShouldReturnZeroAfterAttemptToDecrementCountBelowZero()
-        {
-        }
-
         [TestMethod]
         public void Wait_TimeoutInfinite_ShouldBlockUntilCountdownEventIsSet()
         {
@@ -118,6 +118,7 @@ namespace Renci.SshNet.Tests.Classes.Common
             Assert.IsTrue(actual);
             Assert.AreEqual(expectedSignalCount, signalCount);
             Assert.IsTrue(countdownEvent.IsSet);
+            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
             Assert.IsTrue(elapsedTime >= sleep);
             Assert.IsTrue(elapsedTime <= sleep.Add(TimeSpan.FromMilliseconds(100)));
 
@@ -156,6 +157,7 @@ namespace Renci.SshNet.Tests.Classes.Common
             Assert.IsTrue(actual);
             Assert.AreEqual(expectedSignalCount, signalCount);
             Assert.IsTrue(countdownEvent.IsSet);
+            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
             Assert.IsTrue(elapsedTime >= sleep);
             Assert.IsTrue(elapsedTime <= timeout);
 
@@ -163,7 +165,7 @@ namespace Renci.SshNet.Tests.Classes.Common
         }
 
         [TestMethod]
-        public void Wait_ShouldReturnFalseTimeoutExpiresBeforeCountdownEventIsSet()
+        public void Wait_ShouldReturnFalseWhenTimeoutExpiresBeforeCountdownEventIsSet()
         {
             var sleep = TimeSpan.FromMilliseconds(100);
             var timeout = TimeSpan.FromMilliseconds(30);
@@ -193,6 +195,135 @@ namespace Renci.SshNet.Tests.Classes.Common
 
             Assert.IsFalse(actual);
             Assert.IsFalse(countdownEvent.IsSet);
+            Assert.IsFalse(countdownEvent.WaitHandle.WaitOne(0));
+            Assert.IsTrue(elapsedTime >= timeout);
+
+            countdownEvent.Wait(Session.InfiniteTimeSpan);
+            countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void WaitHandle_ShouldAlwaysReturnSameInstance()
+        {
+            var countdownEvent = CreateCountdownEvent(1);
+
+            var waitHandleA = countdownEvent.WaitHandle;
+            Assert.IsNotNull(waitHandleA);
+
+            var waitHandleB = countdownEvent.WaitHandle;
+            Assert.AreSame(waitHandleA, waitHandleB);
+        }
+
+        [TestMethod]
+        public void WaitHandle_WaitOne_TimeoutInfinite_ShouldBlockUntilCountdownEventIsSet()
+        {
+            var sleep = TimeSpan.FromMilliseconds(100);
+            var timeout = Session.InfiniteTimeSpan;
+
+            var countdownEvent = CreateCountdownEvent(1);
+            var signalCount = 0;
+            var expectedSignalCount = _random.Next(5, 20);
+
+            for (var i = 0; i < (expectedSignalCount - 1); i++)
+                countdownEvent.AddCount();
+
+            var threads = new Thread[expectedSignalCount];
+            for (var i = 0; i < expectedSignalCount; i++)
+            {
+                threads[i] = new Thread(() =>
+                {
+                    Thread.Sleep(sleep);
+                    Interlocked.Increment(ref signalCount);
+                    countdownEvent.Signal();
+                });
+                threads[i].Start();
+            }
+
+            var start = DateTime.Now;
+            var actual = countdownEvent.WaitHandle.WaitOne(timeout);
+            var elapsedTime = DateTime.Now - start;
+
+            Assert.IsTrue(actual);
+            Assert.AreEqual(expectedSignalCount, signalCount);
+            Assert.IsTrue(countdownEvent.IsSet);
+            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
+            Assert.IsTrue(elapsedTime >= sleep);
+            Assert.IsTrue(elapsedTime <= sleep.Add(TimeSpan.FromMilliseconds(100)));
+
+            countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void WaitHandle_WaitOne_ShouldReturnTrueWhenCountdownEventIsSetBeforeTimeoutExpires()
+        {
+            var sleep = TimeSpan.FromMilliseconds(100);
+            var timeout = sleep.Add(TimeSpan.FromSeconds(2));
+
+            var countdownEvent = CreateCountdownEvent(1);
+            var signalCount = 0;
+            var expectedSignalCount = _random.Next(5, 20);
+
+            for (var i = 0; i < (expectedSignalCount - 1); i++)
+                countdownEvent.AddCount();
+
+            var threads = new Thread[expectedSignalCount];
+            for (var i = 0; i < expectedSignalCount; i++)
+            {
+                threads[i] = new Thread(() =>
+                {
+                    Thread.Sleep(sleep);
+                    Interlocked.Increment(ref signalCount);
+                    countdownEvent.Signal();
+                });
+                threads[i].Start();
+            }
+
+            var start = DateTime.Now;
+            var actual = countdownEvent.Wait(timeout);
+            var elapsedTime = DateTime.Now - start;
+
+            Assert.IsTrue(actual);
+            Assert.AreEqual(expectedSignalCount, signalCount);
+            Assert.IsTrue(countdownEvent.IsSet);
+            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
+            Assert.IsTrue(elapsedTime >= sleep);
+            Assert.IsTrue(elapsedTime <= timeout);
+
+            countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void WaitHandle_WaitOne_ShouldReturnFalseWhenTimeoutExpiresBeforeCountdownEventIsSet()
+        {
+            var sleep = TimeSpan.FromMilliseconds(100);
+            var timeout = TimeSpan.FromMilliseconds(30);
+
+            var countdownEvent = CreateCountdownEvent(1);
+            var signalCount = 0;
+            var expectedSignalCount = _random.Next(5, 20);
+
+            for (var i = 0; i < (expectedSignalCount - 1); i++)
+                countdownEvent.AddCount();
+
+            var threads = new Thread[expectedSignalCount];
+            for (var i = 0; i < expectedSignalCount; i++)
+            {
+                threads[i] = new Thread(() =>
+                {
+                    Thread.Sleep(sleep);
+                    countdownEvent.Signal();
+                    Interlocked.Increment(ref signalCount);
+                });
+                threads[i].Start();
+            }
+
+            var start = DateTime.Now;
+            var actual = countdownEvent.WaitHandle.WaitOne(timeout);
+            var elapsedTime = DateTime.Now - start;
+
+            Assert.IsFalse(actual);
+            Assert.IsFalse(countdownEvent.IsSet);
+            Assert.IsFalse(countdownEvent.WaitHandle.WaitOne(0));
             Assert.IsTrue(elapsedTime >= timeout);
 
             countdownEvent.Wait(Session.InfiniteTimeSpan);

+ 109 - 0
src/Renci.SshNet.Tests/Classes/Common/CountdownEventTest_Dispose_NotSet.cs

@@ -0,0 +1,109 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+#if !FEATURE_THREAD_COUNTDOWNEVENT
+using CountdownEvent = Renci.SshNet.Common.CountdownEvent;
+#else
+using System.Threading;
+#endif
+
+namespace Renci.SshNet.Tests.Classes.Common
+{
+    [TestClass]
+    public class CountdownEventTest_Dispose_NotSet
+    {
+        private int _signalsRequired;
+        private CountdownEvent _countdownEvent;
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Arrange()
+        {
+            _signalsRequired = new Random().Next(1, 20);
+            _countdownEvent = new CountdownEvent(_signalsRequired);
+        }
+
+        private void Act()
+        {
+            _countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void AddCount_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                _countdownEvent.AddCount();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void CurrentCount_ShouldReturnRemainingSignalsRequiredToSetEvent()
+        {
+            var actual = _countdownEvent.CurrentCount;
+
+            Assert.AreEqual(_signalsRequired, actual);
+        }
+
+        [TestMethod]
+        public void Dispose_ShouldNotThrow()
+        {
+            _countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void IsSet_ShouldReturnFalse()
+        {
+            var actual = _countdownEvent.IsSet;
+
+            Assert.IsFalse(actual);
+        }
+
+        [TestMethod]
+        public void Signal_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                var set = _countdownEvent.Signal();
+                Assert.Fail("Should have thrown ObjectDisposedException, but returned: " + set);
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void Wait_TimeSpan_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                var set = _countdownEvent.Wait(TimeSpan.FromSeconds(5));
+                Assert.Fail("Should have thrown ObjectDisposedException, but returned: " + set);
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void WaitHandle_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                var waitHandle = _countdownEvent.WaitHandle;
+                Assert.Fail("Should have thrown ObjectDisposedException, but returned: " + waitHandle);
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+    }
+}

+ 107 - 0
src/Renci.SshNet.Tests/Classes/Common/CountdownEventTest_Dispose_Set.cs

@@ -0,0 +1,107 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+#if !FEATURE_THREAD_COUNTDOWNEVENT
+using CountdownEvent = Renci.SshNet.Common.CountdownEvent;
+#else
+using System.Threading;
+#endif
+
+namespace Renci.SshNet.Tests.Classes.Common
+{
+    [TestClass]
+    public class CountdownEventTest_Dispose_Set
+    {
+        private CountdownEvent _countdownEvent;
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Arrange()
+        {
+            _countdownEvent = new CountdownEvent(0);
+        }
+
+        private void Act()
+        {
+            _countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void AddCount_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                _countdownEvent.AddCount();
+                Assert.Fail();
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void CurrentCount_ShouldReturnZero()
+        {
+            var actual = _countdownEvent.CurrentCount;
+
+            Assert.AreEqual(0, actual);
+        }
+
+        [TestMethod]
+        public void Dispose_ShouldNotThrow()
+        {
+            _countdownEvent.Dispose();
+        }
+
+        [TestMethod]
+        public void IsSet_ShouldReturnTrue()
+        {
+            var actual = _countdownEvent.IsSet;
+
+            Assert.IsTrue(actual);
+        }
+
+        [TestMethod]
+        public void Signal_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                var set = _countdownEvent.Signal();
+                Assert.Fail("Should have thrown ObjectDisposedException, but returned: " + set);
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void Wait_TimeSpan_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                var set = _countdownEvent.Wait(TimeSpan.FromSeconds(5));
+                Assert.Fail("Should have thrown ObjectDisposedException, but returned: " + set);
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void WaitHandle_ShouldThrowObjectDisposedException()
+        {
+            try
+            {
+                var waitHandle = _countdownEvent.WaitHandle;
+                Assert.Fail("Should have thrown ObjectDisposedException, but returned: " + waitHandle);
+            }
+            catch (ObjectDisposedException)
+            {
+            }
+        }
+    }
+}

+ 9 - 0
src/Renci.SshNet.Tests/Classes/Common/ExtensionsTest_ShellQuote.cs

@@ -116,5 +116,14 @@ namespace Renci.SshNet.Tests.Classes.Common
 
             Assert.AreEqual("'one\n\ntwo'", actual);
         }
+
+        public void SequenceOfSingleQuoteAndExclamationMark()
+        {
+            var value = "/var/would be 'kewl'!/not?";
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("'/var/would be '\"'\"'kewl'\"'\"\\!'/not?'", actual);
+        }
     }
 }

+ 157 - 0
src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_GetFileName.cs

@@ -0,0 +1,157 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes.Common
+{
+    [TestClass]
+    public class PosixPathTest_GetFileName
+    {
+        [TestMethod]
+        public void Path_Null()
+        {
+            const string path = null;
+
+            try
+            {
+                PosixPath.GetFileName(path);
+                Assert.Fail();
+            }
+            catch (NullReferenceException)
+            {
+            }
+        }
+
+        [TestMethod]
+        public void Path_Empty()
+        {
+            var path = string.Empty;
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreSame(path, actual);
+        }
+
+        [TestMethod]
+        public void Path_TrailingForwardSlash()
+        {
+            var path = "/abc/";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreEqual(string.Empty, actual);
+        }
+
+        [TestMethod]
+        public void Path_FileWithoutNoDirectory()
+        {
+            var path = "abc.log";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreSame(path, actual);
+        }
+
+        [TestMethod]
+        public void Path_FileInRootDirectory()
+        {
+            var path = "/abc.log";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreEqual("abc.log", actual);
+        }
+
+        [TestMethod]
+        public void Path_RootDirectoryOnly()
+        {
+            var path = "/";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreEqual(string.Empty, actual);
+        }
+
+        [TestMethod]
+        public void Path_FileInNonRootDirectory()
+        {
+            var path = "/home/sshnet/xyz";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreEqual("xyz", actual);
+        }
+
+        [TestMethod]
+        public void Path_BackslashIsNotConsideredDirectorySeparator()
+        {
+            var path = "/home\\abc.log";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreEqual("home\\abc.log", actual);
+        }
+
+        [TestMethod]
+        public void Path_ColonIsNotConsideredPathSeparator()
+        {
+            var path = "/home:abc.log";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreEqual("home:abc.log", actual);
+        }
+
+        [TestMethod]
+        public void Path_LeadingWhitespace()
+        {
+            var path = "  / \tabc";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreEqual(" \tabc", actual);
+        }
+
+        [TestMethod]
+        public void Path_TrailingWhitespace()
+        {
+            var path = "/abc \t ";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreEqual("abc \t ", actual);
+        }
+
+        [TestMethod]
+        public void Path_OnlyWhitespace()
+        {
+            var path = " ";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreEqual(" ", actual);
+        }
+
+        [TestMethod]
+        public void Path_FileNameOnlyWhitespace()
+        {
+            var path = "/home/\t ";
+
+            var actual = PosixPath.GetFileName(path);
+
+            Assert.IsNotNull(actual);
+            Assert.AreEqual("\t ", actual);
+        }
+    }
+}

+ 1128 - 0
src/Renci.SshNet.Tests/Classes/RemotePathDoubleQuoteTransformationTest.cs

@@ -0,0 +1,1128 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class RemotePathDoubleQuoteTransformationTest
+    {
+        private IRemotePathTransformation _transformation;
+
+        [TestInitialize]
+        public void SetUp()
+        {
+            _transformation = new RemotePathDoubleQuoteTransformation();
+        }
+
+        /// <summary>
+        /// Test cases from triple-slash comments
+        /// </summary>
+        [TestMethod]
+        public void Mixed()
+        {
+            Assert.AreEqual("\"/var/log/auth.log\"", _transformation.Transform("/var/log/auth.log"));
+            Assert.AreEqual("\"/var/mp3/Guns N' Roses\"", _transformation.Transform("/var/mp3/Guns N' Roses"));
+            Assert.AreEqual("\"/var/garbage!/temp\"", _transformation.Transform("/var/garbage!/temp"));
+            Assert.AreEqual("\"/var/would be 'kewl'!, not?\"", _transformation.Transform("/var/would be 'kewl'!, not?"));
+            Assert.AreEqual("\"\"", _transformation.Transform(string.Empty));
+            Assert.AreEqual("\"Hello \\\"World\\\"\"", _transformation.Transform("Hello \"World\""));
+        }
+
+        [TestMethod]
+        public void Null()
+        {
+            const string path = null;
+
+            try
+            {
+                _transformation.Transform(path);
+                Assert.Fail();
+            }
+            catch (ArgumentNullException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual("path", ex.ParamName);
+            }
+        }
+
+        [TestMethod]
+        public void Ampersand_Embedded()
+        {
+            const string path = "You&Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"You&Me\"", actual);
+        }
+
+        [TestMethod]
+        public void Ampersand_Leading()
+        {
+            const string path = "&Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"&Or\"", actual);
+        }
+
+        [TestMethod]
+        public void Ampersand_LeadingAndTrailing()
+        {
+            const string path = "&Or&";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"&Or&\"", actual);
+        }
+
+        [TestMethod]
+        public void Ampersand_Trailing()
+        {
+            const string path = "And&";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"And&\"", actual);
+        }
+
+        [TestMethod]
+        public void Asterisk_Embedded()
+        {
+            const string path = "Love*Hate";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Love*Hate\"", actual);
+        }
+
+        [TestMethod]
+        public void Asterisk_Leading()
+        {
+            const string path = "*Times";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"*Times\"", actual);
+        }
+
+        [TestMethod]
+        public void Asterisk_LeadingAndTrailing()
+        {
+            const string path = "*WAR*";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"*WAR*\"", actual);
+        }
+
+        [TestMethod]
+        public void Asterisk_Trailing()
+        {
+            const string path = "Censor*";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Censor*\"", actual);
+        }
+
+        [TestMethod]
+        public void Backslash_Embedded()
+        {
+            const string path = "Hello\\World";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Hello\\World\"", actual);
+        }
+
+        [TestMethod]
+        public void Backslash_Leading()
+        {
+            const string path = "\\Hello";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"\\Hello\"", actual);
+        }
+
+        [TestMethod]
+        public void Backslash_LeadingAndTrailing()
+        {
+            const string path = "\\Hello\\";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"\\Hello\\\"", actual);
+        }
+
+        [TestMethod]
+        public void Backslash_Trailing()
+        {
+            const string path = "HelloWorld\\";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"HelloWorld\\\"", actual);
+        }
+
+        [TestMethod]
+        public void Backtick_Embedded()
+        {
+            const string path = "back`tick";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"back`tick\"", actual);
+        }
+
+        [TestMethod]
+        public void Backtick_Leading()
+        {
+            const string path = "`front";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"`front\"", actual);
+        }
+
+        [TestMethod]
+        public void Backtick_LeadingAndTrailing()
+        {
+            const string path = "`FrontAndBack`";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"`FrontAndBack`\"", actual);
+        }
+
+        [TestMethod]
+        public void Backtick_Trailing()
+        {
+            const string path = "back`";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"back`\"", actual);
+        }
+
+        [TestMethod]
+        public void Circumflex_Embedded()
+        {
+            const string path = "You^Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"You^Me\"", actual);
+        }
+
+        [TestMethod]
+        public void Circumflex_Leading()
+        {
+            const string path = "^Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"^Or\"", actual);
+        }
+
+        [TestMethod]
+        public void Circumflex_LeadingAndTrailing()
+        {
+            const string path = "^Or^";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"^Or^\"", actual);
+        }
+
+        [TestMethod]
+        public void Circumflex_Trailing()
+        {
+            const string path = "And^";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"And^\"", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Close_Embedded()
+        {
+            const string path = "Halo}Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Halo}Devine\"", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Close_Leading()
+        {
+            const string path = "}Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"}Open\"", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Close_LeadingAndTrailing()
+        {
+            const string path = "}Closed}";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"}Closed}\"", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Close_Trailing()
+        {
+            const string path = "Finish}";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Finish}\"", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Open_Embedded()
+        {
+            const string path = "Halo{Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Halo{Devine\"", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Open_Leading()
+        {
+            const string path = "{Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"{Open\"", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Open_LeadingAndTrailing()
+        {
+            const string path = "{Closed{";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"{Closed{\"", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Open_Trailing()
+        {
+            const string path = "Finish{";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Finish{\"", actual);
+        }
+
+        [TestMethod]
+        public void Dollar_Embedded()
+        {
+            const string path = "IGiveYouOne$ForYourThoughts";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"IGiveYouOne$ForYourThoughts\"", actual);
+        }
+
+        [TestMethod]
+        public void Dollar_Leading()
+        {
+            const string path = "$Blues";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"$Blues\"", actual);
+        }
+
+        [TestMethod]
+        public void Dollar_LeadingAndTrailing()
+        {
+            const string path = "$SUM$";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"$SUM$\"", actual);
+        }
+
+        [TestMethod]
+        public void Dollar_Trailing()
+        {
+            const string path = "NotCravingForMore$";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"NotCravingForMore$\"", actual);
+        }
+
+        [TestMethod]
+        public void DoubleQuote_Embedded()
+        {
+            const string path = "DoNot\"MeOnThis";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"DoNot\\\"MeOnThis\"", actual);
+        }
+
+        [TestMethod]
+        public void DoubleQuote_Leading()
+        {
+            const string path = "\"OrNotToQuote";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"\\\"OrNotToQuote\"", actual);
+        }
+
+        [TestMethod]
+        public void DoubleQuote_Trailing()
+        {
+            const string path = "Famous\"";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Famous\\\"\"", actual);
+        }
+
+        [TestMethod]
+        public void DoubleQuote_LeadingAndTrailing()
+        {
+            const string path = "\"OrNotTo\"";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"\\\"OrNotTo\\\"\"", actual);
+        }
+
+        [TestMethod]
+        public void Equals_Embedded()
+        {
+            const string path = "You=Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"You=Me\"", actual);
+        }
+
+        [TestMethod]
+        public void Equals_Leading()
+        {
+            const string path = "=Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"=Or\"", actual);
+        }
+
+        [TestMethod]
+        public void Equals_LeadingAndTrailing()
+        {
+            const string path = "=Or=";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"=Or=\"", actual);
+        }
+
+        [TestMethod]
+        public void Equals_Trailing()
+        {
+            const string path = "And=";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"And=\"", actual);
+        }
+
+        [TestMethod]
+        public void ExclamationMark_Embedded()
+        {
+            const string path = "/var/garbage!/temp";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"/var/garbage!/temp\"", actual);
+        }
+
+        [TestMethod]
+        public void ExclamationMark_Leading()
+        {
+            const string path = "!Error";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"!Error\"", actual);
+        }
+
+        [TestMethod]
+        public void ExclamationMark_LeadingAndTrailing()
+        {
+            const string path = "!ignore!";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"!ignore!\"", actual);
+        }
+
+        [TestMethod]
+        public void ExclamationMark_Trailing()
+        {
+            const string path = "Done!";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Done!\"", actual);
+        }
+
+        [TestMethod]
+        public void GreaterThan_Embedded()
+        {
+            const string path = "You>Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"You>Me\"", actual);
+        }
+
+        [TestMethod]
+        public void GreaterThan_Leading()
+        {
+            const string path = ">Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\">Or\"", actual);
+        }
+
+        [TestMethod]
+        public void GreaterThan_LeadingAndTrailing()
+        {
+            const string path = ">Or>";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\">Or>\"", actual);
+        }
+
+        [TestMethod]
+        public void GreaterThan_Trailing()
+        {
+            const string path = "And>";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"And>\"", actual);
+        }
+
+        [TestMethod]
+        public void Hash_Embedded()
+        {
+            const string path = "Smoke#EveryDay";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Smoke#EveryDay\"", actual);
+        }
+
+        [TestMethod]
+        public void Hash_Leading()
+        {
+            const string path = "#4Ever";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"#4Ever\"", actual);
+        }
+
+        [TestMethod]
+        public void Hash_LeadingAndTrailing()
+        {
+            const string path = "#4Ever#";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"#4Ever#\"", actual);
+        }
+
+        [TestMethod]
+        public void Hash_Trailing()
+        {
+            const string path = "Legalize#";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Legalize#\"", actual);
+        }
+
+        [TestMethod]
+        public void LessThan_Embedded()
+        {
+            const string path = "You<Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"You<Me\"", actual);
+        }
+
+        [TestMethod]
+        public void LessThan_Leading()
+        {
+            const string path = "<Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"<Or\"", actual);
+        }
+
+        [TestMethod]
+        public void LessThan_LeadingAndTrailing()
+        {
+            const string path = "<Or<";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"<Or<\"", actual);
+        }
+
+        [TestMethod]
+        public void LessThan_Trailing()
+        {
+            const string path = "And<";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"And<\"", actual);
+        }
+
+        [TestMethod]
+        public void NewLine_Embedded()
+        {
+            const string path = "line\nfeed";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"line\nfeed\"", actual);
+        }
+
+        [TestMethod]
+        public void NewLine_Leading()
+        {
+            const string path = "\nFooter";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"\nFooter\"", actual);
+        }
+
+        [TestMethod]
+        public void NewLine_LeadingAndTrailing()
+        {
+            const string path = "\nBanner\n";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"\nBanner\n\"", actual);
+        }
+
+        [TestMethod]
+        public void NewLine_Trailing()
+        {
+            const string path = "Header\n";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Header\n\"", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Close_Embedded()
+        {
+            const string path = "Halo)Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Halo)Devine\"", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Close_Leading()
+        {
+            const string path = ")Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\")Open\"", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Close_LeadingAndTrailing()
+        {
+            const string path = ")Closed)";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\")Closed)\"", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Close_Trailing()
+        {
+            const string path = "Finish)";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Finish)\"", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Open_Embedded()
+        {
+            const string path = "Halo(Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Halo(Devine\"", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Open_Leading()
+        {
+            const string path = "(Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"(Open\"", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Open_LeadingAndTrailing()
+        {
+            const string path = "(Closed(";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"(Closed(\"", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Open_Trailing()
+        {
+            const string path = "Finish(";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Finish(\"", actual);
+        }
+
+        [TestMethod]
+        public void Percentage_Embedded()
+        {
+            const string path = "Ten%OfOneDollarIsTenCent";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Ten%OfOneDollarIsTenCent\"", actual);
+        }
+
+        [TestMethod]
+        public void Percentage_Leading()
+        {
+            const string path = "%MoreOrLess";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"%MoreOrLess\"", actual);
+        }
+
+        [TestMethod]
+        public void Percentage_LeadingAndTrailing()
+        {
+            const string path = "%USERNAME%";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"%USERNAME%\"", actual);
+        }
+
+        [TestMethod]
+        public void Percentage_Trailing()
+        {
+            const string path = "TakeA%";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"TakeA%\"", actual);
+        }
+
+        [TestMethod]
+        public void Pipe_Embedded()
+        {
+            const string path = "You|Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"You|Me\"", actual);
+        }
+
+        [TestMethod]
+        public void Pipe_Leading()
+        {
+            const string path = "|Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"|Or\"", actual);
+        }
+
+        [TestMethod]
+        public void Pipe_LeadingAndTrailing()
+        {
+            const string path = "|Or|";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"|Or|\"", actual);
+        }
+
+        [TestMethod]
+        public void Pipe_Trailing()
+        {
+            const string path = "And|";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"And|\"", actual);
+        }
+
+        [TestMethod]
+        public void QuestionMark_Embedded()
+        {
+            const string path = "WhatTimeIsIt?SheSaid";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"WhatTimeIsIt?SheSaid\"", actual);
+        }
+
+        [TestMethod]
+        public void QuestionMark_Leading()
+        {
+            const string path = "?Quizz";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"?Quizz\"", actual);
+        }
+
+        [TestMethod]
+        public void QuestionMark_LeadingAndTrailing()
+        {
+            const string path = "?Crazy?";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"?Crazy?\"", actual);
+        }
+
+        [TestMethod]
+        public void QuestionMark_Trailing()
+        {
+            const string path = "WhatTimeIsLove?";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"WhatTimeIsLove?\"", actual);
+        }
+
+        [TestMethod]
+        public void Semicolon_Embedded()
+        {
+            const string path = "Rain;Storm";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Rain;Storm\"", actual);
+        }
+
+        [TestMethod]
+        public void Semicolon_Leading()
+        {
+            const string path = ";List";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\";List\"", actual);
+        }
+
+        [TestMethod]
+        public void Semicolon_LeadingAndTrailing()
+        {
+            const string path = ";Trapped;";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\";Trapped;\"", actual);
+        }
+
+        [TestMethod]
+        public void Semicolon_Trailing()
+        {
+            const string path = "Time;";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Time;\"", actual);
+        }
+
+        [TestMethod]
+        public void SingleQuote_Embedded()
+        {
+            const string path = "Rain'Storm";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Rain'Storm\"", actual);
+        }
+
+        [TestMethod]
+        public void SingleQuote_Leading()
+        {
+            const string path = "'List";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"'List\"", actual);
+        }
+
+        [TestMethod]
+        public void SingleQuote_LeadingAndTrailing()
+        {
+            const string path = "'Trapped'";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"'Trapped'\"", actual);
+        }
+
+        [TestMethod]
+        public void SingleQuote_Trailing()
+        {
+            const string path = "Time'";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Time'\"", actual);
+        }
+
+        [TestMethod]
+        public void Space_Embedded()
+        {
+            const string path = "You Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"You Me\"", actual);
+        }
+
+        [TestMethod]
+        public void Space_Leading()
+        {
+            const string path = " Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\" Or\"", actual);
+        }
+
+        [TestMethod]
+        public void Space_LeadingAndTrailing()
+        {
+            const string path = " Or ";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\" Or \"", actual);
+        }
+
+        [TestMethod]
+        public void Space_Trailing()
+        {
+            const string path = "And ";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"And \"", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Close_Embedded()
+        {
+            const string path = "Halo]Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Halo]Devine\"", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Close_Leading()
+        {
+            const string path = "]Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"]Open\"", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Close_LeadingAndTrailing()
+        {
+            const string path = "]Closed]";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"]Closed]\"", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Close_Trailing()
+        {
+            const string path = "Finish]";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Finish]\"", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Open_Embedded()
+        {
+            const string path = "Halo[Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Halo[Devine\"", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Open_Leading()
+        {
+            const string path = "[Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"[Open\"", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Open_LeadingAndTrailing()
+        {
+            const string path = "[Closed[";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"[Closed[\"", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Open_Trailing()
+        {
+            const string path = "Finish[";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Finish[\"", actual);
+        }
+
+        [TestMethod]
+        public void Tab_Embedded()
+        {
+            const string path = "You\tMe";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"You\tMe\"", actual);
+        }
+
+        [TestMethod]
+        public void Tab_Leading()
+        {
+            const string path = "\tOr";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"\tOr\"", actual);
+        }
+
+        [TestMethod]
+        public void Tab_LeadingAndTrailing()
+        {
+            const string path = "\tOr\t";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"\tOr\t\"", actual);
+        }
+
+        [TestMethod]
+        public void Tab_Trailing()
+        {
+            const string path = "And\t";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"And\t\"", actual);
+        }
+
+        [TestMethod]
+        public void Tilde_Embedded()
+        {
+            const string path = "Seven~Nine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Seven~Nine\"", actual);
+        }
+
+        [TestMethod]
+        public void Tilde_Leading()
+        {
+            const string path = "~Ten";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"~Ten\"", actual);
+        }
+
+        [TestMethod]
+        public void Tilde_LeadingAndTrailing()
+        {
+            const string path = "~One~";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"~One~\"", actual);
+        }
+
+        [TestMethod]
+        public void Tilde_Trailing()
+        {
+            const string path = "Two~";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"Two~\"", actual);
+        }
+    }
+}

+ 1128 - 0
src/Renci.SshNet.Tests/Classes/RemotePathShellQuoteTransformationTest.cs

@@ -0,0 +1,1128 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class RemotePathShellQuoteTransformationTest
+    {
+        private IRemotePathTransformation _transformation;
+
+        [TestInitialize]
+        public void SetUp()
+        {
+            _transformation = new RemotePathShellQuoteTransformation();
+        }
+
+        /// <summary>
+        /// Test cases from triple-slash comments
+        /// </summary>
+        [TestMethod]
+        public void Mixed()
+        {
+            Assert.AreEqual("'/var/log/auth.log'", _transformation.Transform("/var/log/auth.log"));
+            Assert.AreEqual("'/var/mp3/Guns N'\"'\"' Roses'", _transformation.Transform("/var/mp3/Guns N' Roses"));
+            Assert.AreEqual("'/var/garbage'\\!'/temp'", _transformation.Transform("/var/garbage!/temp"));
+            Assert.AreEqual("'/var/would be '\"'\"'kewl'\"'\"\\!', not?'", _transformation.Transform("/var/would be 'kewl'!, not?"));
+            Assert.AreEqual("''", _transformation.Transform(string.Empty));
+            Assert.AreEqual("'Hello \"World\"'", _transformation.Transform("Hello \"World\""));
+        }
+
+        [TestMethod]
+        public void Null()
+        {
+            const string path = null;
+
+            try
+            {
+                _transformation.Transform(path);
+                Assert.Fail();
+            }
+            catch (ArgumentNullException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual("path", ex.ParamName);
+            }
+        }
+
+        [TestMethod]
+        public void Ampersand_Embedded()
+        {
+            const string path = "You&Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'You&Me'", actual);
+        }
+
+        [TestMethod]
+        public void Ampersand_Leading()
+        {
+            const string path = "&Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'&Or'", actual);
+        }
+
+        [TestMethod]
+        public void Ampersand_LeadingAndTrailing()
+        {
+            const string path = "&Or&";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'&Or&'", actual);
+        }
+
+        [TestMethod]
+        public void Ampersand_Trailing()
+        {
+            const string path = "And&";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'And&'", actual);
+        }
+
+        [TestMethod]
+        public void Asterisk_Embedded()
+        {
+            const string path = "Love*Hate";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Love*Hate'", actual);
+        }
+
+        [TestMethod]
+        public void Asterisk_Leading()
+        {
+            const string path = "*Times";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'*Times'", actual);
+        }
+
+        [TestMethod]
+        public void Asterisk_LeadingAndTrailing()
+        {
+            const string path = "*WAR*";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'*WAR*'", actual);
+        }
+
+        [TestMethod]
+        public void Asterisk_Trailing()
+        {
+            const string path = "Censor*";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Censor*'", actual);
+        }
+
+        [TestMethod]
+        public void Backslash_Embedded()
+        {
+            const string path = "Hello\\World";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Hello\\World'", actual);
+        }
+
+        [TestMethod]
+        public void Backslash_Leading()
+        {
+            const string path = "\\Hello";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'\\Hello'", actual);
+        }
+
+        [TestMethod]
+        public void Backslash_LeadingAndTrailing()
+        {
+            const string path = "\\Hello\\";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'\\Hello\\'", actual);
+        }
+
+        [TestMethod]
+        public void Backslash_Trailing()
+        {
+            const string path = "HelloWorld\\";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'HelloWorld\\'", actual);
+        }
+
+        [TestMethod]
+        public void Backtick_Embedded()
+        {
+            const string path = "back`tick";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'back`tick'", actual);
+        }
+
+        [TestMethod]
+        public void Backtick_Leading()
+        {
+            const string path = "`front";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'`front'", actual);
+        }
+
+        [TestMethod]
+        public void Backtick_LeadingAndTrailing()
+        {
+            const string path = "`FrontAndBack`";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'`FrontAndBack`'", actual);
+        }
+
+        [TestMethod]
+        public void Backtick_Trailing()
+        {
+            const string path = "back`";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'back`'", actual);
+        }
+
+        [TestMethod]
+        public void Circumflex_Embedded()
+        {
+            const string path = "You^Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'You^Me'", actual);
+        }
+
+        [TestMethod]
+        public void Circumflex_Leading()
+        {
+            const string path = "^Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'^Or'", actual);
+        }
+
+        [TestMethod]
+        public void Circumflex_LeadingAndTrailing()
+        {
+            const string path = "^Or^";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'^Or^'", actual);
+        }
+
+        [TestMethod]
+        public void Circumflex_Trailing()
+        {
+            const string path = "And^";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'And^'", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Close_Embedded()
+        {
+            const string path = "Halo}Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Halo}Devine'", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Close_Leading()
+        {
+            const string path = "}Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'}Open'", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Close_LeadingAndTrailing()
+        {
+            const string path = "}Closed}";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'}Closed}'", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Close_Trailing()
+        {
+            const string path = "Finish}";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Finish}'", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Open_Embedded()
+        {
+            const string path = "Halo{Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Halo{Devine'", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Open_Leading()
+        {
+            const string path = "{Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'{Open'", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Open_LeadingAndTrailing()
+        {
+            const string path = "{Closed{";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'{Closed{'", actual);
+        }
+
+        [TestMethod]
+        public void CurlyBrackets_Open_Trailing()
+        {
+            const string path = "Finish{";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Finish{'", actual);
+        }
+
+        [TestMethod]
+        public void Dollar_Embedded()
+        {
+            const string path = "IGiveYouOne$ForYourThoughts";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'IGiveYouOne$ForYourThoughts'", actual);
+        }
+
+        [TestMethod]
+        public void Dollar_Leading()
+        {
+            const string path = "$Blues";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'$Blues'", actual);
+        }
+
+        [TestMethod]
+        public void Dollar_LeadingAndTrailing()
+        {
+            const string path = "$SUM$";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'$SUM$'", actual);
+        }
+
+        [TestMethod]
+        public void Dollar_Trailing()
+        {
+            const string path = "NotCravingForMore$";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'NotCravingForMore$'", actual);
+        }
+
+        [TestMethod]
+        public void DoubleQuote_Embedded()
+        {
+            const string path = "DoNot\"MeOnThis";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'DoNot\"MeOnThis'", actual);
+        }
+
+        [TestMethod]
+        public void DoubleQuote_Leading()
+        {
+            const string path = "\"OrNotToQuote";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'\"OrNotToQuote'", actual);
+        }
+
+        [TestMethod]
+        public void DoubleQuote_LeadingAndTrailing()
+        {
+            const string path = "\"OrNotTo\"";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'\"OrNotTo\"'", actual);
+        }
+
+        [TestMethod]
+        public void DoubleQuote_Trailing()
+        {
+            const string path = "Famous\"";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Famous\"'", actual);
+        }
+
+        [TestMethod]
+        public void Equals_Embedded()
+        {
+            const string path = "You=Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'You=Me'", actual);
+        }
+
+        [TestMethod]
+        public void Equals_Leading()
+        {
+            const string path = "=Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'=Or'", actual);
+        }
+
+        [TestMethod]
+        public void Equals_LeadingAndTrailing()
+        {
+            const string path = "=Or=";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'=Or='", actual);
+        }
+
+        [TestMethod]
+        public void Equals_Trailing()
+        {
+            const string path = "And=";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'And='", actual);
+        }
+
+        [TestMethod]
+        public void ExclamationMark_Embedded()
+        {
+            const string path = "/var/garbage!/temp";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'/var/garbage'\\!'/temp'", actual);
+        }
+
+        [TestMethod]
+        public void ExclamationMark_Leading()
+        {
+            const string path = "!Error";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\\!'Error'", actual);
+        }
+
+        [TestMethod]
+        public void ExclamationMark_LeadingAndTrailing()
+        {
+            const string path = "!ignore!";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\\!'ignore'\\!", actual);
+        }
+
+        [TestMethod]
+        public void ExclamationMark_Trailing()
+        {
+            const string path = "Done!";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Done'\\!", actual);
+        }
+
+        [TestMethod]
+        public void GreaterThan_Embedded()
+        {
+            const string path = "You>Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'You>Me'", actual);
+        }
+
+        [TestMethod]
+        public void GreaterThan_Leading()
+        {
+            const string path = ">Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'>Or'", actual);
+        }
+
+        [TestMethod]
+        public void GreaterThan_LeadingAndTrailing()
+        {
+            const string path = ">Or>";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'>Or>'", actual);
+        }
+
+        [TestMethod]
+        public void GreaterThan_Trailing()
+        {
+            const string path = "And>";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'And>'", actual);
+        }
+
+        [TestMethod]
+        public void Hash_Embedded()
+        {
+            const string path = "Smoke#EveryDay";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Smoke#EveryDay'", actual);
+        }
+
+        [TestMethod]
+        public void Hash_Leading()
+        {
+            const string path = "#4Ever";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'#4Ever'", actual);
+        }
+
+        [TestMethod]
+        public void Hash_LeadingAndTrailing()
+        {
+            const string path = "#4Ever#";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'#4Ever#'", actual);
+        }
+
+        [TestMethod]
+        public void Hash_Trailing()
+        {
+            const string path = "Legalize#";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Legalize#'", actual);
+        }
+
+        [TestMethod]
+        public void LessThan_Embedded()
+        {
+            const string path = "You<Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'You<Me'", actual);
+        }
+
+        [TestMethod]
+        public void LessThan_Leading()
+        {
+            const string path = "<Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'<Or'", actual);
+        }
+
+        [TestMethod]
+        public void LessThan_LeadingAndTrailing()
+        {
+            const string path = "<Or<";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'<Or<'", actual);
+        }
+
+        [TestMethod]
+        public void LessThan_Trailing()
+        {
+            const string path = "And<";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'And<'", actual);
+        }
+
+        [TestMethod]
+        public void NewLine_Embedded()
+        {
+            const string path = "line\nfeed";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'line\nfeed'", actual);
+        }
+
+        [TestMethod]
+        public void NewLine_Leading()
+        {
+            const string path = "\nFooter";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'\nFooter'", actual);
+        }
+
+        [TestMethod]
+        public void NewLine_LeadingAndTrailing()
+        {
+            const string path = "\nBanner\n";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'\nBanner\n'", actual);
+        }
+
+        [TestMethod]
+        public void NewLine_Trailing()
+        {
+            const string path = "Header\n";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Header\n'", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Close_Embedded()
+        {
+            const string path = "Halo)Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Halo)Devine'", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Close_Leading()
+        {
+            const string path = ")Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("')Open'", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Close_LeadingAndTrailing()
+        {
+            const string path = ")Closed)";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("')Closed)'", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Close_Trailing()
+        {
+            const string path = "Finish)";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Finish)'", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Open_Embedded()
+        {
+            const string path = "Halo(Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Halo(Devine'", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Open_Leading()
+        {
+            const string path = "(Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'(Open'", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Open_LeadingAndTrailing()
+        {
+            const string path = "(Closed(";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'(Closed('", actual);
+        }
+
+        [TestMethod]
+        public void Parentheses_Open_Trailing()
+        {
+            const string path = "Finish(";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Finish('", actual);
+        }
+
+        [TestMethod]
+        public void Percentage_Embedded()
+        {
+            const string path = "Ten%OfOneDollarIsTenCent";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Ten%OfOneDollarIsTenCent'", actual);
+        }
+
+        [TestMethod]
+        public void Percentage_Leading()
+        {
+            const string path = "%MoreOrLess";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'%MoreOrLess'", actual);
+        }
+
+        [TestMethod]
+        public void Percentage_LeadingAndTrailing()
+        {
+            const string path = "%USERNAME%";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'%USERNAME%'", actual);
+        }
+
+        [TestMethod]
+        public void Percentage_Trailing()
+        {
+            const string path = "TakeA%";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'TakeA%'", actual);
+        }
+
+        [TestMethod]
+        public void Pipe_Embedded()
+        {
+            const string path = "You|Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'You|Me'", actual);
+        }
+
+        [TestMethod]
+        public void Pipe_Leading()
+        {
+            const string path = "|Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'|Or'", actual);
+        }
+
+        [TestMethod]
+        public void Pipe_LeadingAndTrailing()
+        {
+            const string path = "|Or|";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'|Or|'", actual);
+        }
+
+        [TestMethod]
+        public void Pipe_Trailing()
+        {
+            const string path = "And|";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'And|'", actual);
+        }
+
+        [TestMethod]
+        public void QuestionMark_Embedded()
+        {
+            const string path = "WhatTimeIsIt?SheSaid";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'WhatTimeIsIt?SheSaid'", actual);
+        }
+
+        [TestMethod]
+        public void QuestionMark_Leading()
+        {
+            const string path = "?Quizz";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'?Quizz'", actual);
+        }
+
+        [TestMethod]
+        public void QuestionMark_LeadingAndTrailing()
+        {
+            const string path = "?Crazy?";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'?Crazy?'", actual);
+        }
+
+        [TestMethod]
+        public void QuestionMark_Trailing()
+        {
+            const string path = "WhatTimeIsLove?";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'WhatTimeIsLove?'", actual);
+        }
+
+        [TestMethod]
+        public void Semicolon_Embedded()
+        {
+            const string path = "Rain;Storm";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Rain;Storm'", actual);
+        }
+
+        [TestMethod]
+        public void Semicolon_Leading()
+        {
+            const string path = ";List";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("';List'", actual);
+        }
+
+        [TestMethod]
+        public void Semicolon_LeadingAndTrailing()
+        {
+            const string path = ";Trapped;";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("';Trapped;'", actual);
+        }
+
+        [TestMethod]
+        public void Semicolon_Trailing()
+        {
+            const string path = "Time;";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Time;'", actual);
+        }
+
+        [TestMethod]
+        public void SingleQuote_Embedded()
+        {
+            const string path = "Rain'Storm";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Rain'\"'\"'Storm'", actual);
+        }
+
+        [TestMethod]
+        public void SingleQuote_Leading()
+        {
+            const string path = "'List";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"'\"'List'", actual);
+        }
+
+        [TestMethod]
+        public void SingleQuote_LeadingAndTrailing()
+        {
+            const string path = "'Trapped'";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("\"'\"'Trapped'\"'\"", actual);
+        }
+
+        [TestMethod]
+        public void SingleQuote_Trailing()
+        {
+            const string path = "Time'";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Time'\"'\"", actual);
+        }
+
+        [TestMethod]
+        public void Space_Embedded()
+        {
+            const string path = "You Me";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'You Me'", actual);
+        }
+
+        [TestMethod]
+        public void Space_Leading()
+        {
+            const string path = " Or";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("' Or'", actual);
+        }
+
+        [TestMethod]
+        public void Space_LeadingAndTrailing()
+        {
+            const string path = " Or ";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("' Or '", actual);
+        }
+
+        [TestMethod]
+        public void Space_Trailing()
+        {
+            const string path = "And ";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'And '", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Close_Embedded()
+        {
+            const string path = "Halo]Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Halo]Devine'", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Close_Leading()
+        {
+            const string path = "]Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("']Open'", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Close_LeadingAndTrailing()
+        {
+            const string path = "]Closed]";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("']Closed]'", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Close_Trailing()
+        {
+            const string path = "Finish]";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Finish]'", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Open_Embedded()
+        {
+            const string path = "Halo[Devine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Halo[Devine'", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Open_Leading()
+        {
+            const string path = "[Open";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'[Open'", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Open_LeadingAndTrailing()
+        {
+            const string path = "[Closed[";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'[Closed['", actual);
+        }
+
+        [TestMethod]
+        public void SquareBrackets_Open_Trailing()
+        {
+            const string path = "Finish[";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Finish['", actual);
+        }
+
+        [TestMethod]
+        public void Tab_Embedded()
+        {
+            const string path = "You\tMe";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'You\tMe'", actual);
+        }
+
+        [TestMethod]
+        public void Tab_Leading()
+        {
+            const string path = "\tOr";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'\tOr'", actual);
+        }
+
+        [TestMethod]
+        public void Tab_LeadingAndTrailing()
+        {
+            const string path = "\tOr\t";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'\tOr\t'", actual);
+        }
+
+        [TestMethod]
+        public void Tab_Trailing()
+        {
+            const string path = "And\t";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'And\t'", actual);
+        }
+
+        [TestMethod]
+        public void Tilde_Embedded()
+        {
+            const string path = "Seven~Nine";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Seven~Nine'", actual);
+        }
+
+        [TestMethod]
+        public void Tilde_Leading()
+        {
+            const string path = "~Ten";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'~Ten'", actual);
+        }
+
+        [TestMethod]
+        public void Tilde_LeadingAndTrailing()
+        {
+            const string path = "~One~";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'~One~'", actual);
+        }
+
+        [TestMethod]
+        public void Tilde_Trailing()
+        {
+            const string path = "Two~";
+
+            var actual = _transformation.Transform(path);
+
+            Assert.AreEqual("'Two~'", actual);
+        }
+    }
+}

+ 228 - 78
src/Renci.SshNet.Tests/Classes/ScpClientTest.cs

@@ -15,6 +15,206 @@ namespace Renci.SshNet.Tests.Classes
     [TestClass]
     public partial class ScpClientTest : TestBase
     {
+        private Random _random;
+
+        [TestInitialize]
+        public void SetUp()
+        {
+            _random = new Random();
+        }
+
+        [TestMethod]
+        public void Ctor_ConnectionInfo_Null()
+        {
+            const ConnectionInfo connectionInfo = null;
+
+            try
+            {
+                new ScpClient(connectionInfo);
+                Assert.Fail();
+            }
+            catch (ArgumentNullException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual("connectionInfo", ex.ParamName);
+            }
+        }
+
+        [TestMethod]
+        public void Ctor_ConnectionInfo_NotNull()
+        {
+            var connectionInfo = new ConnectionInfo("HOST", "USER", new PasswordAuthenticationMethod("USER", "PWD"));
+
+            var client = new ScpClient(connectionInfo);
+            Assert.AreEqual(16 * 1024U, client.BufferSize);
+            Assert.AreSame(connectionInfo, client.ConnectionInfo);
+            Assert.IsFalse(client.IsConnected);
+            Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.KeepAliveInterval);
+            Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.OperationTimeout);
+            Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation);
+            Assert.IsNull(client.Session);
+        }
+
+        [TestMethod]
+        public void Ctor_HostAndPortAndUsernameAndPassword()
+        {
+            var host = _random.Next().ToString();
+            var port = _random.Next(1, 100);
+            var userName = _random.Next().ToString();
+            var password = _random.Next().ToString();
+
+            var client = new ScpClient(host, port, userName, password);
+            Assert.AreEqual(16 * 1024U, client.BufferSize);
+            Assert.IsNotNull(client.ConnectionInfo);
+            Assert.IsFalse(client.IsConnected);
+            Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.KeepAliveInterval);
+            Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.OperationTimeout);
+            Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation);
+            Assert.IsNull(client.Session);
+
+            var passwordConnectionInfo = client.ConnectionInfo as PasswordConnectionInfo;
+            Assert.IsNotNull(passwordConnectionInfo);
+            Assert.AreEqual(host, passwordConnectionInfo.Host);
+            Assert.AreEqual(port, passwordConnectionInfo.Port);
+            Assert.AreSame(userName, passwordConnectionInfo.Username);
+            Assert.IsNotNull(passwordConnectionInfo.AuthenticationMethods);
+            Assert.AreEqual(1, passwordConnectionInfo.AuthenticationMethods.Count);
+
+            var passwordAuthentication = passwordConnectionInfo.AuthenticationMethods[0] as PasswordAuthenticationMethod;
+            Assert.IsNotNull(passwordAuthentication);
+            Assert.AreEqual(userName, passwordAuthentication.Username);
+            Assert.IsTrue(Encoding.UTF8.GetBytes(password).IsEqualTo(passwordAuthentication.Password));
+        }
+
+        [TestMethod]
+        public void Ctor_HostAndUsernameAndPassword()
+        {
+            var host = _random.Next().ToString();
+            var userName = _random.Next().ToString();
+            var password = _random.Next().ToString();
+
+            var client = new ScpClient(host, userName, password);
+            Assert.AreEqual(16 * 1024U, client.BufferSize);
+            Assert.IsNotNull(client.ConnectionInfo);
+            Assert.IsFalse(client.IsConnected);
+            Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.KeepAliveInterval);
+            Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.OperationTimeout);
+            Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation);
+            Assert.IsNull(client.Session);
+
+            var passwordConnectionInfo = client.ConnectionInfo as PasswordConnectionInfo;
+            Assert.IsNotNull(passwordConnectionInfo);
+            Assert.AreEqual(host, passwordConnectionInfo.Host);
+            Assert.AreEqual(22, passwordConnectionInfo.Port);
+            Assert.AreSame(userName, passwordConnectionInfo.Username);
+            Assert.IsNotNull(passwordConnectionInfo.AuthenticationMethods);
+            Assert.AreEqual(1, passwordConnectionInfo.AuthenticationMethods.Count);
+
+            var passwordAuthentication = passwordConnectionInfo.AuthenticationMethods[0] as PasswordAuthenticationMethod;
+            Assert.IsNotNull(passwordAuthentication);
+            Assert.AreEqual(userName, passwordAuthentication.Username);
+            Assert.IsTrue(Encoding.UTF8.GetBytes(password).IsEqualTo(passwordAuthentication.Password));
+        }
+
+        [TestMethod]
+        public void Ctor_HostAndPortAndUsernameAndPrivateKeys()
+        {
+            var host = _random.Next().ToString();
+            var port = _random.Next(1, 100);
+            var userName = _random.Next().ToString();
+            var privateKeys = new[] {GetRsaKey(), GetDsaKey()};
+
+            var client = new ScpClient(host, port, userName, privateKeys);
+            Assert.AreEqual(16 * 1024U, client.BufferSize);
+            Assert.IsNotNull(client.ConnectionInfo);
+            Assert.IsFalse(client.IsConnected);
+            Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.KeepAliveInterval);
+            Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.OperationTimeout);
+            Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation);
+            Assert.IsNull(client.Session);
+
+            var privateKeyConnectionInfo = client.ConnectionInfo as PrivateKeyConnectionInfo;
+            Assert.IsNotNull(privateKeyConnectionInfo);
+            Assert.AreEqual(host, privateKeyConnectionInfo.Host);
+            Assert.AreEqual(port, privateKeyConnectionInfo.Port);
+            Assert.AreSame(userName, privateKeyConnectionInfo.Username);
+            Assert.IsNotNull(privateKeyConnectionInfo.AuthenticationMethods);
+            Assert.AreEqual(1, privateKeyConnectionInfo.AuthenticationMethods.Count);
+
+            var privateKeyAuthentication = privateKeyConnectionInfo.AuthenticationMethods[0] as PrivateKeyAuthenticationMethod;
+            Assert.IsNotNull(privateKeyAuthentication);
+            Assert.AreEqual(userName, privateKeyAuthentication.Username);
+            Assert.IsNotNull(privateKeyAuthentication.KeyFiles);
+            Assert.AreEqual(privateKeys.Length, privateKeyAuthentication.KeyFiles.Count);
+            Assert.IsTrue(privateKeyAuthentication.KeyFiles.Contains(privateKeys[0]));
+            Assert.IsTrue(privateKeyAuthentication.KeyFiles.Contains(privateKeys[1]));
+        }
+
+        [TestMethod]
+        public void Ctor_HostAndUsernameAndPrivateKeys()
+        {
+            var host = _random.Next().ToString();
+            var userName = _random.Next().ToString();
+            var privateKeys = new[] { GetRsaKey(), GetDsaKey() };
+
+            var client = new ScpClient(host, userName, privateKeys);
+            Assert.AreEqual(16 * 1024U, client.BufferSize);
+            Assert.IsNotNull(client.ConnectionInfo);
+            Assert.IsFalse(client.IsConnected);
+            Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.KeepAliveInterval);
+            Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.OperationTimeout);
+            Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation);
+            Assert.IsNull(client.Session);
+
+            var privateKeyConnectionInfo = client.ConnectionInfo as PrivateKeyConnectionInfo;
+            Assert.IsNotNull(privateKeyConnectionInfo);
+            Assert.AreEqual(host, privateKeyConnectionInfo.Host);
+            Assert.AreEqual(22, privateKeyConnectionInfo.Port);
+            Assert.AreSame(userName, privateKeyConnectionInfo.Username);
+            Assert.IsNotNull(privateKeyConnectionInfo.AuthenticationMethods);
+            Assert.AreEqual(1, privateKeyConnectionInfo.AuthenticationMethods.Count);
+
+            var privateKeyAuthentication = privateKeyConnectionInfo.AuthenticationMethods[0] as PrivateKeyAuthenticationMethod;
+            Assert.IsNotNull(privateKeyAuthentication);
+            Assert.AreEqual(userName, privateKeyAuthentication.Username);
+            Assert.IsNotNull(privateKeyAuthentication.KeyFiles);
+            Assert.AreEqual(privateKeys.Length, privateKeyAuthentication.KeyFiles.Count);
+            Assert.IsTrue(privateKeyAuthentication.KeyFiles.Contains(privateKeys[0]));
+            Assert.IsTrue(privateKeyAuthentication.KeyFiles.Contains(privateKeys[1]));
+        }
+
+        [TestMethod]
+        public void RemotePathTransformation_Value_NotNull()
+        {
+            var client = new ScpClient("HOST", 22, "USER", "PWD");
+
+            Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation);
+            client.RemotePathTransformation = RemotePathTransformation.ShellQuote;
+            Assert.AreSame(RemotePathTransformation.ShellQuote, client.RemotePathTransformation);
+        }
+
+        [TestMethod]
+        public void RemotePathTransformation_Value_Null()
+        {
+            var client = new ScpClient("HOST", 22, "USER", "PWD")
+            {
+                RemotePathTransformation = RemotePathTransformation.ShellQuote
+            };
+
+            try
+            {
+                client.RemotePathTransformation = null;
+                Assert.Fail();
+            }
+            catch (ArgumentNullException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual("value", ex.ParamName);
+            }
+
+            Assert.AreSame(RemotePathTransformation.ShellQuote, client.RemotePathTransformation);
+        }
+
         [TestMethod]
         [TestCategory("Scp")]
         [TestCategory("integration")]
@@ -172,10 +372,12 @@ namespace Renci.SshNet.Tests.Classes
             {
                 scp.Connect();
 
-                var uploadDirectory = Directory.CreateDirectory(string.Format("{0}\\{1}", Path.GetTempPath(), Path.GetRandomFileName()));
+                var uploadDirectory =
+                    Directory.CreateDirectory(string.Format("{0}\\{1}", Path.GetTempPath(), Path.GetRandomFileName()));
                 for (int i = 0; i < 3; i++)
                 {
-                    var subfolder = Directory.CreateDirectory(string.Format(@"{0}\folder_{1}", uploadDirectory.FullName, i));
+                    var subfolder =
+                        Directory.CreateDirectory(string.Format(@"{0}\folder_{1}", uploadDirectory.FullName, i));
                     for (int j = 0; j < 5; j++)
                     {
                         this.CreateTestFile(string.Format(@"{0}\file_{1}", subfolder.FullName, j), 1);
@@ -185,7 +387,8 @@ namespace Renci.SshNet.Tests.Classes
 
                 scp.Upload(uploadDirectory, "uploaded_dir");
 
-                var downloadDirectory = Directory.CreateDirectory(string.Format("{0}\\{1}", Path.GetTempPath(), Path.GetRandomFileName()));
+                var downloadDirectory =
+                    Directory.CreateDirectory(string.Format("{0}\\{1}", Path.GetTempPath(), Path.GetRandomFileName()));
 
                 scp.Download("uploaded_dir", downloadDirectory);
 
@@ -193,11 +396,12 @@ namespace Renci.SshNet.Tests.Classes
                 var downloadFiles = downloadDirectory.GetFiles("*.*", System.IO.SearchOption.AllDirectories);
 
                 var result = from f1 in uploadedFiles
-                             from f2 in downloadFiles
-                             where
-                                f1.FullName.Substring(uploadDirectory.FullName.Length) == f2.FullName.Substring(downloadDirectory.FullName.Length)
-                                && CalculateMD5(f1.FullName) == CalculateMD5(f2.FullName)
-                             select f1;
+                    from f2 in downloadFiles
+                    where
+                    f1.FullName.Substring(uploadDirectory.FullName.Length) ==
+                    f2.FullName.Substring(downloadDirectory.FullName.Length)
+                    && CalculateMD5(f1.FullName) == CalculateMD5(f2.FullName)
+                    select f1;
 
                 var counter = result.Count();
 
@@ -331,76 +535,6 @@ namespace Renci.SshNet.Tests.Classes
             Assert.Inconclusive("A method that does not return a value cannot be verified.");
         }
 
-        /// <summary>
-        ///A test for ScpClient Constructor
-        ///</summary>
-        [TestMethod]
-        [Ignore] // placeholder for actual test
-        public void ScpClientConstructorTest()
-        {
-            string host = string.Empty; // TODO: Initialize to an appropriate value
-            string username = string.Empty; // TODO: Initialize to an appropriate value
-            PrivateKeyFile[] keyFiles = null; // TODO: Initialize to an appropriate value
-            ScpClient target = new ScpClient(host, username, keyFiles);
-            Assert.Inconclusive("TODO: Implement code to verify target");
-        }
-
-        /// <summary>
-        ///A test for ScpClient Constructor
-        ///</summary>
-        [TestMethod]
-        [Ignore] // placeholder for actual test
-        public void ScpClientConstructorTest1()
-        {
-            string host = string.Empty; // TODO: Initialize to an appropriate value
-            int port = 0; // TODO: Initialize to an appropriate value
-            string username = string.Empty; // TODO: Initialize to an appropriate value
-            PrivateKeyFile[] keyFiles = null; // TODO: Initialize to an appropriate value
-            ScpClient target = new ScpClient(host, port, username, keyFiles);
-            Assert.Inconclusive("TODO: Implement code to verify target");
-        }
-
-        /// <summary>
-        ///A test for ScpClient Constructor
-        ///</summary>
-        [TestMethod]
-        [Ignore] // placeholder for actual test
-        public void ScpClientConstructorTest2()
-        {
-            string host = string.Empty; // TODO: Initialize to an appropriate value
-            string username = string.Empty; // TODO: Initialize to an appropriate value
-            string password = string.Empty; // TODO: Initialize to an appropriate value
-            ScpClient target = new ScpClient(host, username, password);
-            Assert.Inconclusive("TODO: Implement code to verify target");
-        }
-
-        /// <summary>
-        ///A test for ScpClient Constructor
-        ///</summary>
-        [TestMethod]
-        [Ignore] // placeholder for actual test
-        public void ScpClientConstructorTest3()
-        {
-            string host = string.Empty; // TODO: Initialize to an appropriate value
-            int port = 0; // TODO: Initialize to an appropriate value
-            string username = string.Empty; // TODO: Initialize to an appropriate value
-            string password = string.Empty; // TODO: Initialize to an appropriate value
-            ScpClient target = new ScpClient(host, port, username, password);
-            Assert.Inconclusive("TODO: Implement code to verify target");
-        }
-
-        /// <summary>
-        ///A test for ScpClient Constructor
-        ///</summary>
-        [TestMethod]
-        [Ignore] // placeholder for actual test
-        public void ScpClientConstructorTest4()
-        {
-            ConnectionInfo connectionInfo = null; // TODO: Initialize to an appropriate value
-            ScpClient target = new ScpClient(connectionInfo);
-            Assert.Inconclusive("TODO: Implement code to verify target");
-        }
-
         protected static string CalculateMD5(string fileName)
         {
             using (var file = new FileStream(fileName, FileMode.Open))
@@ -427,5 +561,21 @@ namespace Renci.SshNet.Tests.Classes
                 client.Disconnect();
             }
         }
+
+        private PrivateKeyFile GetRsaKey()
+        {
+            using (var stream = GetData("Key.RSA.txt"))
+            {
+                return new PrivateKeyFile(stream);
+            }
+        }
+
+        private PrivateKeyFile GetDsaKey()
+        {
+            using (var stream = GetData("Key.SSH2.DSA.txt"))
+            {
+                return new PrivateKeyFile(stream);
+            }
+        }
     }
 }

+ 45 - 0
src/Renci.SshNet.Tests/Classes/ScpClientTestBase.cs

@@ -0,0 +1,45 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Channels;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    public abstract class ScpClientTestBase
+    {
+        internal Mock<IServiceFactory> _serviceFactoryMock;
+        internal Mock<IRemotePathTransformation> _remotePathTransformationMock;
+        internal Mock<ISession> _sessionMock;
+        internal Mock<IChannelSession> _channelSessionMock;
+        internal Mock<PipeStream> _pipeStreamMock;
+
+        protected abstract void SetupData();
+
+        protected void CreateMocks()
+        {
+            _serviceFactoryMock = new Mock<IServiceFactory>(MockBehavior.Strict);
+            _remotePathTransformationMock = new Mock<IRemotePathTransformation>(MockBehavior.Strict);
+            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
+            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
+            _pipeStreamMock = new Mock<PipeStream>(MockBehavior.Strict);
+        }
+
+        protected abstract void SetupMocks();
+
+        protected virtual void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+        }
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        protected abstract void Act();
+    }
+}

+ 26 - 26
src/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse.cs

@@ -4,66 +4,66 @@ using System.Globalization;
 using System.IO;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
-using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
 
 namespace Renci.SshNet.Tests.Classes
 {
     [TestClass]
-    public class ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse
+    public class ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse : ScpClientTestBase
     {
-        private Mock<IServiceFactory> _serviceFactoryMock;
-        private Mock<ISession> _sessionMock;
-        private Mock<IChannelSession> _channelSessionMock;
-        private Mock<PipeStream> _pipeStreamMock;
         private ConnectionInfo _connectionInfo;
         private ScpClient _scpClient;
         private DirectoryInfo _directoryInfo;
         private string _path;
-        private string _quotedPath;
+        private string _transformedPath;
         private IList<ScpUploadEventArgs> _uploadingRegister;
         private SshException _actualException;
 
-        [TestInitialize]
-        public void Setup()
-        {
-            Arrange();
-            Act();
-        }
-
-        protected void Arrange()
+        protected override void SetupData()
         {
             var random = new Random();
+
             _connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
             _directoryInfo = new DirectoryInfo("destination");
             _path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
-            _quotedPath = _path.ShellQuote();
+            _transformedPath = random.Next().ToString();
             _uploadingRegister = new List<ScpUploadEventArgs>();
+        }
 
-            _serviceFactoryMock = new Mock<IServiceFactory>(MockBehavior.Strict);
-            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
-            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
-            _pipeStreamMock = new Mock<PipeStream>(MockBehavior.Strict);
-
+        protected override void SetupMocks()
+        {
             var sequence = new MockSequence();
+
+            _serviceFactoryMock.InSequence(sequence)
+                               .Setup(p => p.CreateRemotePathDoubleQuoteTransformation())
+                               .Returns(_remotePathTransformationMock.Object);
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateSession(_connectionInfo))
-                .Returns(_sessionMock.Object);
+                               .Setup(p => p.CreateSession(_connectionInfo))
+                               .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence).Setup(p => p.CreatePipeStream()).Returns(_pipeStreamMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Open());
+            _remotePathTransformationMock.InSequence(sequence)
+                                         .Setup(p => p.Transform(_path))
+                                         .Returns(_transformedPath);
             _channelSessionMock.InSequence(sequence)
-                .Setup(p => p.SendExecRequest(string.Format("scp -prf {0}", _quotedPath))).Returns(false);
+                               .Setup(p => p.SendExecRequest(string.Format("scp -prf {0}", _transformedPath)))
+                               .Returns(false);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Dispose());
             _pipeStreamMock.As<IDisposable>().InSequence(sequence).Setup(p => p.Dispose());
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
 
             _scpClient = new ScpClient(_connectionInfo, false, _serviceFactoryMock.Object);
             _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args);
             _scpClient.Connect();
         }
 
-        protected virtual void Act()
+        protected override void Act()
         {
             try
             {
@@ -87,7 +87,7 @@ namespace Renci.SshNet.Tests.Classes
         [TestMethod]
         public void SendExecRequestOnChannelSessionShouldBeInvokedOnce()
         {
-            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -prf {0}", _quotedPath)), Times.Once);
+            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -prf {0}", _transformedPath)), Times.Once);
         }
 
         [TestMethod]

+ 25 - 26
src/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndFileInfo_SendExecRequestReturnsFalse.cs

@@ -4,66 +4,65 @@ using System.Globalization;
 using System.IO;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
-using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
 
 namespace Renci.SshNet.Tests.Classes
 {
     [TestClass]
-    public class ScpClientTest_Download_PathAndFileInfo_SendExecRequestReturnsFalse
+    public class ScpClientTest_Download_PathAndFileInfo_SendExecRequestReturnsFalse : ScpClientTestBase
     {
-        private Mock<IServiceFactory> _serviceFactoryMock;
-        private Mock<ISession> _sessionMock;
-        private Mock<IChannelSession> _channelSessionMock;
-        private Mock<PipeStream> _pipeStreamMock;
         private ConnectionInfo _connectionInfo;
         private ScpClient _scpClient;
         private FileInfo _fileInfo;
         private string _path;
-        private string _quotedPath;
+        private string _transformedPath;
         private IList<ScpUploadEventArgs> _uploadingRegister;
         private SshException _actualException;
 
-        [TestInitialize]
-        public void Setup()
-        {
-            Arrange();
-            Act();
-        }
-
-        protected void Arrange()
+        protected override void SetupData()
         {
             var random = new Random();
+
             _connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
             _fileInfo = new FileInfo("destination");
             _path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
-            _quotedPath = _path.ShellQuote();
+            _transformedPath = random.Next().ToString();
             _uploadingRegister = new List<ScpUploadEventArgs>();
+        }
 
-            _serviceFactoryMock = new Mock<IServiceFactory>(MockBehavior.Strict);
-            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
-            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
-            _pipeStreamMock = new Mock<PipeStream>(MockBehavior.Strict);
-
+        protected override void SetupMocks()
+        {
             var sequence = new MockSequence();
+
+            _serviceFactoryMock.InSequence(sequence)
+                               .Setup(p => p.CreateRemotePathDoubleQuoteTransformation())
+                               .Returns(_remotePathTransformationMock.Object);
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateSession(_connectionInfo))
-                .Returns(_sessionMock.Object);
+                               .Setup(p => p.CreateSession(_connectionInfo))
+                               .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence).Setup(p => p.CreatePipeStream()).Returns(_pipeStreamMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Open());
+            _remotePathTransformationMock.InSequence(sequence)
+                                         .Setup(p => p.Transform(_path))
+                                         .Returns(_transformedPath);
             _channelSessionMock.InSequence(sequence)
-                .Setup(p => p.SendExecRequest(string.Format("scp -pf {0}", _quotedPath))).Returns(false);
+                .Setup(p => p.SendExecRequest(string.Format("scp -pf {0}", _transformedPath))).Returns(false);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Dispose());
             _pipeStreamMock.As<IDisposable>().InSequence(sequence).Setup(p => p.Dispose());
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
 
             _scpClient = new ScpClient(_connectionInfo, false, _serviceFactoryMock.Object);
             _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args);
             _scpClient.Connect();
         }
 
-        protected virtual void Act()
+        protected override void Act()
         {
             try
             {
@@ -87,7 +86,7 @@ namespace Renci.SshNet.Tests.Classes
         [TestMethod]
         public void SendExecRequestOnChannelSessionShouldBeInvokedOnce()
         {
-            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -pf {0}", _quotedPath)), Times.Once);
+            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -pf {0}", _transformedPath)), Times.Once);
         }
 
         [TestMethod]

+ 26 - 26
src/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndStream_SendExecRequestReturnsFalse.cs

@@ -4,33 +4,21 @@ using System.Globalization;
 using System.IO;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
-using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
 
 namespace Renci.SshNet.Tests.Classes
 {
     [TestClass]
-    public class ScpClientTest_Download_PathAndStream_SendExecRequestReturnsFalse
+    public class ScpClientTest_Download_PathAndStream_SendExecRequestReturnsFalse : ScpClientTestBase
     {
-        private Mock<IServiceFactory> _serviceFactoryMock;
-        private Mock<ISession> _sessionMock;
-        private Mock<IChannelSession> _channelSessionMock;
-        private Mock<PipeStream> _pipeStreamMock;
         private ConnectionInfo _connectionInfo;
         private ScpClient _scpClient;
         private Stream _destination;
         private string _path;
-        private string _quotedPath;
+        private string _transformedPath;
         private IList<ScpUploadEventArgs> _uploadingRegister;
         private SshException _actualException;
 
-        [TestInitialize]
-        public void Setup()
-        {
-            Arrange();
-            Act();
-        }
-
         [TestCleanup]
         public void Cleanup()
         {
@@ -40,39 +28,51 @@ namespace Renci.SshNet.Tests.Classes
             }
         }
 
-        protected void Arrange()
+        protected override void SetupData()
         {
             var random = new Random();
+
             _connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
             _destination = new MemoryStream();
             _path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
-            _quotedPath = _path.ShellQuote();
+            _transformedPath = random.Next().ToString();
             _uploadingRegister = new List<ScpUploadEventArgs>();
+        }
 
-            _serviceFactoryMock = new Mock<IServiceFactory>(MockBehavior.Strict);
-            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
-            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
-            _pipeStreamMock = new Mock<PipeStream>(MockBehavior.Strict);
-
+        protected override void SetupMocks()
+        {
             var sequence = new MockSequence();
+
+            _serviceFactoryMock.InSequence(sequence)
+                               .Setup(p => p.CreateRemotePathDoubleQuoteTransformation())
+                               .Returns(_remotePathTransformationMock.Object);
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateSession(_connectionInfo))
-                .Returns(_sessionMock.Object);
+                               .Setup(p => p.CreateSession(_connectionInfo))
+                               .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence).Setup(p => p.CreatePipeStream()).Returns(_pipeStreamMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Open());
+            _remotePathTransformationMock.InSequence(sequence)
+                                         .Setup(p => p.Transform(_path))
+                                         .Returns(_transformedPath);
             _channelSessionMock.InSequence(sequence)
-                .Setup(p => p.SendExecRequest(string.Format("scp -f {0}", _quotedPath))).Returns(false);
+                               .Setup(p => p.SendExecRequest(string.Format("scp -f {0}", _transformedPath)))
+                               .Returns(false);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Dispose());
             _pipeStreamMock.As<IDisposable>().InSequence(sequence).Setup(p => p.Dispose());
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
 
             _scpClient = new ScpClient(_connectionInfo, false, _serviceFactoryMock.Object);
             _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args);
             _scpClient.Connect();
         }
 
-        protected virtual void Act()
+        protected override void Act()
         {
             try
             {
@@ -96,7 +96,7 @@ namespace Renci.SshNet.Tests.Classes
         [TestMethod]
         public void SendExecRequestOnChannelSessionShouldBeInvokedOnce()
         {
-            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -f {0}", _quotedPath)), Times.Once);
+            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -f {0}", _transformedPath)), Times.Once);
         }
 
         [TestMethod]

+ 26 - 26
src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse.cs

@@ -4,66 +4,66 @@ using System.Globalization;
 using System.IO;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
-using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
 
 namespace Renci.SshNet.Tests.Classes
 {
     [TestClass]
-    public class ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse
+    public class ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse : ScpClientTestBase
     {
-        private Mock<IServiceFactory> _serviceFactoryMock;
-        private Mock<ISession> _sessionMock;
-        private Mock<IChannelSession> _channelSessionMock;
-        private Mock<PipeStream> _pipeStreamMock;
         private ConnectionInfo _connectionInfo;
         private ScpClient _scpClient;
         private DirectoryInfo _directoryInfo;
         private string _path;
-        private string _quotedPath;
+        private string _transformedPath;
         private IList<ScpUploadEventArgs> _uploadingRegister;
         private SshException _actualException;
 
-        [TestInitialize]
-        public void Setup()
-        {
-            Arrange();
-            Act();
-        }
-
-        protected void Arrange()
+        protected override void SetupData()
         {
             var random = new Random();
+
             _connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
             _directoryInfo = new DirectoryInfo("source");
             _path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
-            _quotedPath = _path.ShellQuote();
+            _transformedPath = random.Next().ToString();
             _uploadingRegister = new List<ScpUploadEventArgs>();
+        }
 
-            _serviceFactoryMock = new Mock<IServiceFactory>(MockBehavior.Strict);
-            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
-            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
-            _pipeStreamMock = new Mock<PipeStream>(MockBehavior.Strict);
-
+        protected override void SetupMocks()
+        {
             var sequence = new MockSequence();
+
+            _serviceFactoryMock.InSequence(sequence)
+                               .Setup(p => p.CreateRemotePathDoubleQuoteTransformation())
+                               .Returns(_remotePathTransformationMock.Object);
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateSession(_connectionInfo))
-                .Returns(_sessionMock.Object);
+                               .Setup(p => p.CreateSession(_connectionInfo))
+                               .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence).Setup(p => p.CreatePipeStream()).Returns(_pipeStreamMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Open());
+            _remotePathTransformationMock.InSequence(sequence)
+                                         .Setup(p => p.Transform(_path))
+                                         .Returns(_transformedPath);
             _channelSessionMock.InSequence(sequence)
-                .Setup(p => p.SendExecRequest(string.Format("scp -rt {0}", _quotedPath))).Returns(false);
+                               .Setup(p => p.SendExecRequest(string.Format("scp -rt {0}", _transformedPath)))
+                               .Returns(false);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Dispose());
             _pipeStreamMock.As<IDisposable>().InSequence(sequence).Setup(p => p.Dispose());
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
 
             _scpClient = new ScpClient(_connectionInfo, false, _serviceFactoryMock.Object);
             _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args);
             _scpClient.Connect();
         }
 
-        protected virtual void Act()
+        protected override void Act()
         {
             try
             {
@@ -87,7 +87,7 @@ namespace Renci.SshNet.Tests.Classes
         [TestMethod]
         public void SendExecREquestOnChannelSessionShouldBeInvokedOnce()
         {
-            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -rt {0}", _quotedPath)), Times.Once);
+            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -rt {0}", _transformedPath)), Times.Once);
         }
 
         [TestMethod]

+ 27 - 27
src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs

@@ -4,34 +4,22 @@ using System.Globalization;
 using System.IO;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
-using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
 
 namespace Renci.SshNet.Tests.Classes
 {
     [TestClass]
-    public class ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse
+    public class ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse : ScpClientTestBase
     {
-        private Mock<IServiceFactory> _serviceFactoryMock;
-        private Mock<ISession> _sessionMock;
-        private Mock<IChannelSession> _channelSessionMock;
-        private Mock<PipeStream> _pipeStreamMock;
         private ConnectionInfo _connectionInfo;
         private ScpClient _scpClient;
         private FileInfo _fileInfo;
         private string _path;
-        private string _quotedPath;
+        private string _transformedPath;
         private string _fileName;
         private IList<ScpUploadEventArgs> _uploadingRegister;
         private SshException _actualException;
 
-        [TestInitialize]
-        public void Setup()
-        {
-            Arrange();
-            Act();
-        }
-
         [TestCleanup]
         public void Cleanup()
         {
@@ -42,40 +30,52 @@ namespace Renci.SshNet.Tests.Classes
             }
         }
 
-        protected void Arrange()
+        protected override void SetupData()
         {
             var random = new Random();
-            _fileName = CreateTemporaryFile(new byte[] {1});
+
+            _fileName = CreateTemporaryFile(new byte[] { 1 });
             _connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
             _fileInfo = new FileInfo(_fileName);
             _path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
-            _quotedPath = _path.ShellQuote();
+            _transformedPath = _path.ShellQuote();
             _uploadingRegister = new List<ScpUploadEventArgs>();
+        }
 
-            _serviceFactoryMock = new Mock<IServiceFactory>(MockBehavior.Strict);
-            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
-            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
-            _pipeStreamMock = new Mock<PipeStream>(MockBehavior.Strict);
-
+        protected override void SetupMocks()
+        {
             var sequence = new MockSequence();
+
+            _serviceFactoryMock.InSequence(sequence)
+                               .Setup(p => p.CreateRemotePathDoubleQuoteTransformation())
+                               .Returns(_remotePathTransformationMock.Object);
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateSession(_connectionInfo))
-                .Returns(_sessionMock.Object);
+                               .Setup(p => p.CreateSession(_connectionInfo))
+                               .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence).Setup(p => p.CreatePipeStream()).Returns(_pipeStreamMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Open());
+            _remotePathTransformationMock.InSequence(sequence)
+                                         .Setup(p => p.Transform(_path))
+                                         .Returns(_transformedPath);
             _channelSessionMock.InSequence(sequence)
-                               .Setup(p => p.SendExecRequest(string.Format("scp -t {0}", _quotedPath))).Returns(false);
+                               .Setup(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)))
+                               .Returns(false);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Dispose());
             _pipeStreamMock.As<IDisposable>().InSequence(sequence).Setup(p => p.Dispose());
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
 
             _scpClient = new ScpClient(_connectionInfo, false, _serviceFactoryMock.Object);
             _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args);
             _scpClient.Connect();
         }
 
-        protected virtual void Act()
+        protected override void Act()
         {
             try
             {
@@ -99,7 +99,7 @@ namespace Renci.SshNet.Tests.Classes
         [TestMethod]
         public void SendExecRequestOnChannelSessionShouldBeInvokedOnce()
         {
-            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t {0}", _quotedPath)), Times.Once);
+            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)), Times.Once);
         }
 
         [TestMethod]

+ 20 - 34
src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs

@@ -6,36 +6,24 @@ using System.Linq;
 using System.Text;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
-using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
 
 namespace Renci.SshNet.Tests.Classes
 {
     [TestClass]
-    public class ScpClientTest_Upload_FileInfoAndPath_Success
+    public class ScpClientTest_Upload_FileInfoAndPath_Success : ScpClientTestBase
     {
-        private Mock<IServiceFactory> _serviceFactoryMock;
-        private Mock<ISession> _sessionMock;
-        private Mock<IChannelSession> _channelSessionMock;
-        private Mock<PipeStream> _pipeStreamMock;
         private ConnectionInfo _connectionInfo;
         private ScpClient _scpClient;
         private FileInfo _fileInfo;
         private string _path;
-        private string _quotedPath;
+        private string _transformedPath;
         private int _bufferSize;
         private byte[] _fileContent;
         private string _fileName;
         private int _fileSize;
         private IList<ScpUploadEventArgs> _uploadingRegister;
 
-        [TestInitialize]
-        public void Setup()
-        {
-            Arrange();
-            Act();
-        }
-
         [TestCleanup]
         public void Cleanup()
         {
@@ -46,7 +34,7 @@ namespace Renci.SshNet.Tests.Classes
             }
         }
 
-        private void SetupData()
+        protected override void SetupData()
         {
             var random = new Random();
 
@@ -57,30 +45,30 @@ namespace Renci.SshNet.Tests.Classes
             _connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
             _fileInfo = new FileInfo(_fileName);
             _path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
-            _quotedPath = _path.ShellQuote();
+            _transformedPath = random.Next().ToString();
             _uploadingRegister = new List<ScpUploadEventArgs>();
         }
 
-        private void CreateMocks()
-        {
-            _serviceFactoryMock = new Mock<IServiceFactory>(MockBehavior.Strict);
-            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
-            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
-            _pipeStreamMock = new Mock<PipeStream>(MockBehavior.Strict);
-        }
-
-        private void SetupMocks()
+        protected override void SetupMocks()
         {
             var sequence = new MockSequence();
+
+            _serviceFactoryMock.InSequence(sequence)
+                               .Setup(p => p.CreateRemotePathDoubleQuoteTransformation())
+                               .Returns(_remotePathTransformationMock.Object);
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateSession(_connectionInfo))
-                .Returns(_sessionMock.Object);
+                               .Setup(p => p.CreateSession(_connectionInfo))
+                               .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence).Setup(p => p.CreatePipeStream()).Returns(_pipeStreamMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Open());
+            _remotePathTransformationMock.InSequence(sequence)
+                                         .Setup(p => p.Transform(_path))
+                                         .Returns(_transformedPath);
             _channelSessionMock.InSequence(sequence)
-                               .Setup(p => p.SendExecRequest(string.Format("scp -t {0}", _quotedPath))).Returns(true);
+                               .Setup(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)))
+                               .Returns(true);
             _pipeStreamMock.InSequence(sequence).Setup(p => p.ReadByte()).Returns(0);
             _channelSessionMock.InSequence(sequence).Setup(p => p.SendData(It.IsAny<byte[]>()));
             _pipeStreamMock.InSequence(sequence).Setup(p => p.ReadByte()).Returns(0);
@@ -102,11 +90,9 @@ namespace Renci.SshNet.Tests.Classes
             _pipeStreamMock.As<IDisposable>().InSequence(sequence).Setup(p => p.Dispose());
         }
 
-        protected void Arrange()
+        protected override void Arrange()
         {
-            SetupData();
-            CreateMocks();
-            SetupMocks();
+            base.Arrange();
 
             _scpClient = new ScpClient(_connectionInfo, false, _serviceFactoryMock.Object)
                 {
@@ -116,7 +102,7 @@ namespace Renci.SshNet.Tests.Classes
             _scpClient.Connect();
         }
 
-        protected virtual void Act()
+        protected override void Act()
         {
             _scpClient.Upload(_fileInfo, _path);
         }
@@ -124,7 +110,7 @@ namespace Renci.SshNet.Tests.Classes
         [TestMethod]
         public void SendExecRequestOnChannelSessionShouldBeInvokedOnce()
         {
-            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t {0}", _quotedPath)), Times.Once);
+            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)), Times.Once);
         }
 
         [TestMethod]

+ 26 - 26
src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse.cs

@@ -4,33 +4,21 @@ using System.Globalization;
 using System.IO;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
-using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
 
 namespace Renci.SshNet.Tests.Classes
 {
     [TestClass]
-    public class ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse
+    public class ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse : ScpClientTestBase
     {
-        private Mock<IServiceFactory> _serviceFactoryMock;
-        private Mock<ISession> _sessionMock;
-        private Mock<IChannelSession> _channelSessionMock;
-        private Mock<PipeStream> _pipeStreamMock;
         private ConnectionInfo _connectionInfo;
         private ScpClient _scpClient;
         private Stream _source;
         private string _path;
-        private string _quotedPath;
+        private string _transformedPath;
         private IList<ScpUploadEventArgs> _uploadingRegister;
         private SshException _actualException;
 
-        [TestInitialize]
-        public void Setup()
-        {
-            Arrange();
-            Act();
-        }
-
         [TestCleanup]
         public void Cleanup()
         {
@@ -40,39 +28,51 @@ namespace Renci.SshNet.Tests.Classes
             }
         }
 
-        protected void Arrange()
+        protected override void SetupData()
         {
             var random = new Random();
+
             _connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
             _source = new MemoryStream();
             _path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
-            _quotedPath = _path.ShellQuote();
+            _transformedPath = random.Next().ToString();
             _uploadingRegister = new List<ScpUploadEventArgs>();
+        }
 
-            _serviceFactoryMock = new Mock<IServiceFactory>(MockBehavior.Strict);
-            _sessionMock = new Mock<ISession>(MockBehavior.Strict);
-            _channelSessionMock = new Mock<IChannelSession>(MockBehavior.Strict);
-            _pipeStreamMock = new Mock<PipeStream>(MockBehavior.Strict);
-
+        protected override void SetupMocks()
+        {
             var sequence = new MockSequence();
+
+            _serviceFactoryMock.InSequence(sequence)
+                               .Setup(p => p.CreateRemotePathDoubleQuoteTransformation())
+                               .Returns(_remotePathTransformationMock.Object);
             _serviceFactoryMock.InSequence(sequence)
-                .Setup(p => p.CreateSession(_connectionInfo))
-                .Returns(_sessionMock.Object);
+                               .Setup(p => p.CreateSession(_connectionInfo))
+                               .Returns(_sessionMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.Connect());
             _serviceFactoryMock.InSequence(sequence).Setup(p => p.CreatePipeStream()).Returns(_pipeStreamMock.Object);
             _sessionMock.InSequence(sequence).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Open());
+            _remotePathTransformationMock.InSequence(sequence)
+                                         .Setup(p => p.Transform(_path))
+                                         .Returns(_transformedPath);
             _channelSessionMock.InSequence(sequence)
-                .Setup(p => p.SendExecRequest(string.Format("scp -t {0}", _quotedPath))).Returns(false);
+                               .Setup(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)))
+                               .Returns(false);
             _channelSessionMock.InSequence(sequence).Setup(p => p.Dispose());
             _pipeStreamMock.As<IDisposable>().InSequence(sequence).Setup(p => p.Dispose());
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
 
             _scpClient = new ScpClient(_connectionInfo, false, _serviceFactoryMock.Object);
             _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args);
             _scpClient.Connect();
         }
 
-        protected virtual void Act()
+        protected override void Act()
         {
             try
             {
@@ -96,7 +96,7 @@ namespace Renci.SshNet.Tests.Classes
         [TestMethod]
         public void SendExecRequestOnChannelSessionShouldBeInvokedOnce()
         {
-            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t {0}", _quotedPath)), Times.Once);
+            _channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)), Times.Once);
         }
 
         [TestMethod]

+ 0 - 1
src/Renci.SshNet.Tests/Classes/Sftp/SftpFileStreamTest_Flush_WriteMode_NoDataInBuffer.cs

@@ -19,7 +19,6 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private uint _readBufferSize;
         private uint _writeBufferSize;
         private byte[] _writeBytes;
-        private byte[] _requestWriteBytes;
 
         protected override void SetupData()
         {

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

@@ -19,7 +19,6 @@ namespace Renci.SshNet.Tests.Classes.Sftp
         private byte[] _handle;
         private SftpFileStream _target;
         private int _offset;
-        private EndOfStreamException _actualException;
         private long _actual;
 
         protected override void SetupData()

+ 6 - 0
src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj

@@ -144,6 +144,8 @@
     <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs" />
     <Compile Include="Classes\ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs" />
     <Compile Include="Classes\Common\CountdownEventTest.cs" />
+    <Compile Include="Classes\Common\CountdownEventTest_Dispose_NotSet.cs" />
+    <Compile Include="Classes\Common\CountdownEventTest_Dispose_Set.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_Concat.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_IsEqualTo_ByteArray.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_Reverse.cs" />
@@ -152,6 +154,7 @@
     <Compile Include="Classes\Common\ExtensionsTest_Take_OffsetAndCount.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_TrimLeadingZeros.cs" />
     <Compile Include="Classes\Common\PackTest.cs" />
+    <Compile Include="Classes\Common\PosixPathTest_GetFileName.cs" />
     <Compile Include="Classes\ConnectionInfoTest_Authenticate_Failure.cs" />
     <Compile Include="Classes\ConnectionInfoTest_Authenticate_Success.cs" />
     <Compile Include="Classes\ForwardedPortDynamicTest_Dispose_PortStarted_ChannelBound.cs" />
@@ -211,6 +214,9 @@
     <Compile Include="Classes\NetConfClientTest_Dispose_Disposed.cs" />
     <Compile Include="Classes\NetConfClientTest_Finalize_Connected.cs" />
     <Compile Include="Classes\PipeStreamTest_Dispose.cs" />
+    <Compile Include="Classes\RemotePathDoubleQuoteTransformationTest.cs" />
+    <Compile Include="Classes\RemotePathShellQuoteTransformationTest.cs" />
+    <Compile Include="Classes\ScpClientTestBase.cs" />
     <Compile Include="Classes\ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse.cs" />
     <Compile Include="Classes\ScpClientTest_Download_PathAndFileInfo_SendExecRequestReturnsFalse.cs" />
     <Compile Include="Classes\ScpClientTest_Download_PathAndStream_SendExecRequestReturnsFalse.cs" />

+ 15 - 0
src/Renci.SshNet.UAP10/Renci.SshNet.UAP10.csproj

@@ -369,6 +369,9 @@
     <Compile Include="..\Renci.SshNet\IForwardedPort.cs">
       <Link>IForwardedPort.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\IRemotePathTransformation.cs">
+      <Link>IRemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\IServiceFactory.cs">
       <Link>IServiceFactory.cs</Link>
     </Compile>
@@ -642,6 +645,18 @@
     <Compile Include="..\Renci.SshNet\ProxyTypes.cs">
       <Link>ProxyTypes.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathDoubleQuoteTransformation.cs">
+      <Link>RemotePathDoubleQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathNoneTransformation.cs">
+      <Link>RemotePathNoneTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathShellQuoteTransformation.cs">
+      <Link>RemotePathShellQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathTransformation.cs">
+      <Link>RemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\ScpClient.cs">
       <Link>ScpClient.cs</Link>
     </Compile>

+ 15 - 0
src/Renci.SshNet.WindowsPhone/Renci.SshNet.WindowsPhone.csproj

@@ -305,6 +305,9 @@
     <Compile Include="..\Renci.SshNet\IForwardedPort.cs">
       <Link>IForwardedPort.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\IRemotePathTransformation.cs">
+      <Link>IRemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\IServiceFactory.cs">
       <Link>IServiceFactory.cs</Link>
     </Compile>
@@ -563,6 +566,18 @@
     <Compile Include="..\Renci.SshNet\ProxyTypes.cs">
       <Link>ProxyTypes.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathDoubleQuoteTransformation.cs">
+      <Link>RemotePathDoubleQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathNoneTransformation.cs">
+      <Link>RemotePathNoneTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathShellQuoteTransformation.cs">
+      <Link>RemotePathShellQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathTransformation.cs">
+      <Link>RemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\ScpClient.cs">
       <Link>ScpClient.cs</Link>
     </Compile>

+ 16 - 1
src/Renci.SshNet.WindowsPhone8/Renci.SshNet.WindowsPhone8.csproj

@@ -352,6 +352,9 @@
     <Compile Include="..\Renci.SshNet\IForwardedPort.cs">
       <Link>IForwardedPort.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\IRemotePathTransformation.cs">
+      <Link>IRemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\IServiceFactory.cs">
       <Link>IServiceFactory.cs</Link>
     </Compile>
@@ -610,6 +613,18 @@
     <Compile Include="..\Renci.SshNet\ProxyTypes.cs">
       <Link>ProxyTypes.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathDoubleQuoteTransformation.cs">
+      <Link>RemotePathDoubleQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathNoneTransformation.cs">
+      <Link>RemotePathNoneTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathShellQuoteTransformation.cs">
+      <Link>RemotePathShellQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathTransformation.cs">
+      <Link>RemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\ScpClient.cs">
       <Link>ScpClient.cs</Link>
     </Compile>
@@ -982,7 +997,7 @@
   <Import Project="$(MSBuildExtensionsPath)\Microsoft\$(TargetFrameworkIdentifier)\$(TargetFrameworkVersion)\Microsoft.$(TargetFrameworkIdentifier).CSharp.targets" />
   <ProjectExtensions>
     <VisualStudio>
-      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
+      <UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 18 - 0
src/Renci.SshNet/Common/CountdownEvent.cs

@@ -57,6 +57,24 @@ namespace Renci.SshNet.Common
             get { return _count == 0; }
         }
 
+        /// <summary>
+        /// Gets a <see cref="WaitHandle"/> that is used to wait for the event to be set.
+        /// </summary>
+        /// <value>
+        /// A <see cref="WaitHandle"/> that is used to wait for the event to be set.
+        /// </value>
+        /// <exception cref="ObjectDisposedException">The current instance has already been disposed.</exception>
+        public WaitHandle WaitHandle
+        {
+            get
+            {
+                EnsureNotDisposed();
+
+                return _event;
+            }
+        }
+
+
         /// <summary>
         /// Registers a signal with the <see cref="CountdownEvent"/>, decrementing the value of <see cref="CurrentCount"/>.
         /// </summary>

+ 14 - 4
src/Renci.SshNet/Common/PosixPath.cs

@@ -1,4 +1,6 @@
-namespace Renci.SshNet.Common
+using System;
+
+namespace Renci.SshNet.Common
 {
     internal class PosixPath
     {
@@ -9,15 +11,23 @@
         /// <returns>
         /// The file name part of <paramref name="path"/>.
         /// </returns>
+        /// <exception cref="NullReferenceException"><paramref name="path"/> is <c>null</c>.</exception>
         /// <remarks>
-        /// If <paramref name="path"/> contains no forward slash or has a trailing
-        /// forward slash, then <paramref name="path"/> is returned.
+        /// <para>
+        /// If <paramref name="path"/> contains no forward slash, then <paramref name="path"/>
+        /// is returned.
+        /// </para>
+        /// <para>
+        /// If path has a trailing slash, but <see cref="GetFileName(string)"/> return a zero-length string.
+        /// </para>
         /// </remarks>
         public static string GetFileName(string path)
         {
             var pathEnd = path.LastIndexOf('/');
-            if (pathEnd == -1 || pathEnd == path.Length - 1)
+            if (pathEnd == -1)
                 return path;
+            if (pathEnd == path.Length - 1)
+                return string.Empty;
             return path.Substring(pathEnd + 1);
         }
     }

+ 19 - 0
src/Renci.SshNet/IRemotePathTransformation.cs

@@ -0,0 +1,19 @@
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Represents a transformation that can be applied to a remote path.
+    /// </summary>
+    public interface IRemotePathTransformation
+    {
+        /// <summary>
+        /// Transforms the specified remote path.
+        /// </summary>
+        /// <param name="path">The path to transform.</param>
+        /// <returns>
+        /// The transformed path.
+        /// </returns>
+        string Transform(string path);
+    }
+
+
+}

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

@@ -62,5 +62,16 @@ namespace Renci.SshNet
         ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize);
 
         ISftpResponseFactory CreateSftpResponseFactory();
+
+        /// <summary>
+        /// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
+        /// any embedded double quote with a backslash.
+        /// </summary>
+        /// <returns>
+        /// An <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes any
+        /// embedded double quote with a backslash.
+        /// with a shell.
+        /// </returns>
+        IRemotePathTransformation CreateRemotePathDoubleQuoteTransformation();
     }
 }

+ 11 - 0
src/Renci.SshNet/PasswordAuthenticationMethod.cs

@@ -28,6 +28,17 @@ namespace Renci.SshNet
             get { return _requestMessage.MethodName; }
         }
 
+        /// <summary>
+        /// Gets the password as a sequence of bytes.
+        /// </summary>
+        /// <value>
+        /// The password as a sequence of bytes.
+        /// </value>
+        internal byte[] Password
+        {
+            get { return _password; }
+        }
+
         /// <summary>
         /// Occurs when user's password has expired and needs to be changed.
         /// </summary>

+ 72 - 0
src/Renci.SshNet/RemotePathDoubleQuoteTransformation.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Text;
+
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Encloses a path in double quotes, and escapes any embedded double quote with a backslash.
+    /// </summary>
+    internal class RemotePathDoubleQuoteTransformation : IRemotePathTransformation
+    {
+        /// <summary>
+        /// Encloses a path in double quotes, and escapes any embedded double quote with a backslash.
+        /// </summary>
+        /// <param name="path">The path to transform.</param>
+        /// <returns>
+        /// The transformed path.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
+        /// <example>
+        /// <list type="table">
+        ///   <listheader>
+        ///     <term>Original</term>
+        ///     <term>Transformed</term>
+        ///   </listheader>
+        ///   <item>
+        ///     <term>/var/log/auth.log</term>
+        ///     <term>&quot;/var/log/auth.log&quot;</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/mp3/Guns N' Roses</term>
+        ///     <term>&quot;/var/mp3/Guns N' Roses&quot;</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/garbage!/temp</term>
+        ///     <term>&quot;/var/garbage!/temp&quot;</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/would be 'kewl'!/not?</term>
+        ///     <term>&quot;/var/would be 'kewl'!, not?&quot;</term>
+        ///   </item>
+        ///   <item>
+        ///     <term></term>
+        ///     <term>&quot;&quot;</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>Hello &quot;World&quot;</term>
+        ///     <term>&quot;Hello \&quot;World&quot;</term>
+        ///   </item>
+        /// </list>
+        /// </example>
+        public string Transform(string path)
+        {
+            if (path == null)
+            {
+                throw new ArgumentNullException("path");
+            }
+
+            var transformed = new StringBuilder(path.Length);
+
+            transformed.Append('"');
+            foreach (var c in path)
+            {
+                if (c == '"')
+                    transformed.Append('\\');
+                transformed.Append(c);
+            }
+            transformed.Append('"');
+
+            return transformed.ToString();
+        }
+    }
+}

+ 32 - 0
src/Renci.SshNet/RemotePathNoneTransformation.cs

@@ -0,0 +1,32 @@
+using System;
+
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Performs no transformation.
+    /// </summary>
+    internal class RemotePathNoneTransformation : IRemotePathTransformation
+    {
+        /// <summary>
+        /// Returns the specified path without applying a transformation.
+        /// </summary>
+        /// <param name="path">The path to transform.</param>
+        /// <returns>
+        /// The specified path as is.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
+        /// <remarks>
+        /// This transformation is recommended for servers that do not require any quoting to preserve the
+        /// literal value of metacharacters, or when paths are guaranteed to never contain any such characters.
+        /// </remarks>
+        public string Transform(string path)
+        {
+            if (path == null)
+            {
+                throw new ArgumentNullException("path");
+            }
+
+            return path;
+        }
+    }
+}

+ 193 - 0
src/Renci.SshNet/RemotePathShellQuoteTransformation.cs

@@ -0,0 +1,193 @@
+using System;
+using System.Text;
+
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Quotes a path in a way to be suitable to be used with a shell-based server.
+    /// </summary>
+    internal class RemotePathShellQuoteTransformation : IRemotePathTransformation
+    {
+        /// <summary>
+        /// Quotes a path in a way to be suitable to be used with a shell-based server.
+        /// </summary>
+        /// <param name="path">The path to transform.</param>
+        /// <returns>
+        /// A quoted path.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
+        /// <remarks>
+        /// <para>
+        /// If <paramref name="path"/> contains a single-quote, that character is embedded
+        /// in quotation marks (eg. "'"). Sequences of single-quotes are grouped in a single
+        /// pair of quotation marks.
+        /// </para>
+        /// <para>
+        /// An exclamation mark in <paramref name="path"/> is escaped with a backslash. This is
+        /// necessary because C Shell interprets it as a meta-character for history substitution
+        /// even when enclosed in single quotes or quotation marks.
+        /// </para>
+        /// <para>
+        /// All other characters are enclosed in single quotes. Sequences of such characters are grouped
+        /// in a single pair of single quotes.
+        /// </para>
+        /// <para>
+        /// References:
+        /// <list type="bullet">
+        ///   <item>
+        ///     <description><a href="http://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html">Shell Command Language</a></description>
+        ///   </item>
+        ///   <item>
+        ///     <description><a href="https://earthsci.stanford.edu/computing/unix/shell/specialchars.php">Unix C-Shell special characters and their uses</a></description>
+        ///   </item>
+        ///   <item>
+        ///     <description><a href="https://docstore.mik.ua/orelly/unix3/upt/ch27_13.htm">Differences Between Bourne and C Shell Quoting</a></description>
+        ///   </item>
+        /// </list>
+        /// </para>
+        /// </remarks>
+        /// <example>
+        /// <list type="table">
+        ///   <listheader>
+        ///     <term>Original</term>
+        ///     <term>Transformed</term>
+        ///   </listheader>
+        ///   <item>
+        ///     <term>/var/log/auth.log</term>
+        ///     <term>'/var/log/auth.log'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/mp3/Guns N' Roses</term>
+        ///     <term>'/var/mp3/Guns N'"'"' Roses'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/garbage!/temp</term>
+        ///     <term>'/var/garbage'\!'/temp'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/would be 'kewl'!, not?</term>
+        ///     <term>'/var/would be '"'"'kewl'"'"\!', not?'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term></term>
+        ///     <term>''</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>Hello &quot;World&quot;</term>
+        ///     <term>'Hello "World"'</term>
+        ///   </item>
+        /// </list>
+        /// </example>
+        public string Transform(string path)
+        {
+            if (path == null)
+            {
+                throw new ArgumentNullException("path");
+            }
+
+            // result is at least value and (likely) leading/trailing single-quotes
+            var sb = new StringBuilder(path.Length + 2);
+            var state = ShellQuoteState.Unquoted;
+
+            foreach (var c in path)
+            {
+                switch (c)
+                {
+                    case '\'':
+                        // embed a single-quote in quotes
+                        switch (state)
+                        {
+                            case ShellQuoteState.Unquoted:
+                                // Start quoted string
+                                sb.Append('"');
+                                break;
+                            case ShellQuoteState.Quoted:
+                                // Continue quoted string
+                                break;
+                            case ShellQuoteState.SingleQuoted:
+                                // Close single-quoted string
+                                sb.Append('\'');
+                                // Start quoted string
+                                sb.Append('"');
+                                break;
+                        }
+                        state = ShellQuoteState.Quoted;
+                        break;
+                    case '!':
+                        // In C-Shell, an exclamatation point can only be protected from shell interpretation
+                        // when escaped by a backslash
+                        // Source:
+                        // https://earthsci.stanford.edu/computing/unix/shell/specialchars.php
+
+                        switch (state)
+                        {
+                            case ShellQuoteState.Unquoted:
+                                sb.Append('\\');
+                                break;
+                            case ShellQuoteState.Quoted:
+                                // Close quoted string
+                                sb.Append('"');
+                                sb.Append('\\');
+                                break;
+                            case ShellQuoteState.SingleQuoted:
+                                // Close single quoted string
+                                sb.Append('\'');
+                                sb.Append('\\');
+                                break;
+                        }
+                        state = ShellQuoteState.Unquoted;
+                        break;
+                    default:
+                        switch (state)
+                        {
+                            case ShellQuoteState.Unquoted:
+                                // Start single-quoted string
+                                sb.Append('\'');
+                                break;
+                            case ShellQuoteState.Quoted:
+                                // Close quoted string
+                                sb.Append('"');
+                                // Start single-quoted string
+                                sb.Append('\'');
+                                break;
+                            case ShellQuoteState.SingleQuoted:
+                                // Continue single-quoted string
+                                break;
+                        }
+                        state = ShellQuoteState.SingleQuoted;
+                        break;
+                }
+
+                sb.Append(c);
+            }
+
+            switch (state)
+            {
+                case ShellQuoteState.Unquoted:
+                    break;
+                case ShellQuoteState.Quoted:
+                    // Close quoted string
+                    sb.Append('"');
+                    break;
+                case ShellQuoteState.SingleQuoted:
+                    // Close single-quoted string
+                    sb.Append('\'');
+                    break;
+            }
+
+            if (sb.Length == 0)
+            {
+                sb.Append("''");
+            }
+
+            return sb.ToString();
+        }
+
+        private enum ShellQuoteState
+        {
+            Unquoted = 1,
+            SingleQuoted = 2,
+            Quoted = 3
+        }
+    }
+}

+ 143 - 0
src/Renci.SshNet/RemotePathTransformation.cs

@@ -0,0 +1,143 @@
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Provides access to built-in remote path transformations.
+    /// </summary>
+    /// <remarks>
+    /// References:
+    /// <list type="bullet">
+    ///   <item>
+    ///     <description><a href="http://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html">Shell Command Language</a></description>
+    ///   </item>
+    ///   <item>
+    ///     <description><a href="https://earthsci.stanford.edu/computing/unix/shell/specialchars.php">Unix C-Shell special characters and their uses</a></description>
+    ///   </item>
+    ///   <item>
+    ///     <description><a href="https://docstore.mik.ua/orelly/unix3/upt/ch27_13.htm">Differences Between Bourne and C Shell Quoting</a></description>
+    ///   </item>
+    ///   <item>
+    ///     <description><a href="https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/">Everyone quotes command line arguments the wrong way</a></description>
+    ///   </item>
+    /// </list>
+    /// </remarks>
+    public static class RemotePathTransformation
+    {
+        private static readonly IRemotePathTransformation ShellQuoteTransformation = new RemotePathShellQuoteTransformation();
+        private static readonly IRemotePathTransformation NoneTransformation = new RemotePathNoneTransformation();
+        private static readonly IRemotePathTransformation DoubleQuoteTransformation = new RemotePathDoubleQuoteTransformation();
+
+        /// <summary>
+        /// Quotes a path in a way to be suitable to be used with a shell-based server.
+        /// </summary>
+        /// <returns>
+        /// A quoted path.
+        /// </returns>
+        /// <remarks>
+        /// <para>
+        /// If a path contains a single-quote, that character is embedded in quotation marks (eg. "'").
+        /// Sequences of single-quotes are grouped in a single pair of quotation marks.
+        /// </para>
+        /// <para>
+        /// An exclamation mark in a path is escaped with a backslash. This is necessary because C Shell
+        /// interprets it as a meta-character for history substitution even when enclosed in single quotes
+        ///  or quotation marks.
+        /// </para>
+        /// <para>
+        /// All other characters are enclosed in single quotes. Sequences of such characters are grouped
+        /// in a single pair of single quotes.
+        /// </para>
+        /// </remarks>
+        /// <example>
+        /// <list type="table">
+        ///   <listheader>
+        ///     <term>Original</term>
+        ///     <term>Transformed</term>
+        ///   </listheader>
+        ///   <item>
+        ///     <term>/var/log/auth.log</term>
+        ///     <term>'/var/log/auth.log'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/mp3/Guns N' Roses</term>
+        ///     <term>'/var/mp3/Guns N'"'"' Roses'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/garbage!/temp</term>
+        ///     <term>'/var/garbage'\!'/temp'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/would be 'kewl'!, not?</term>
+        ///     <term>'/var/would be '"'"'kewl'"'"\!', not?'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term></term>
+        ///     <term>''</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>Hello &quot;World&quot;</term>
+        ///     <term>'Hello "World"'</term>
+        ///   </item>
+        /// </list>
+        /// </example>
+        public static IRemotePathTransformation ShellQuote
+        {
+            get { return ShellQuoteTransformation; }
+        }
+
+        /// <summary>
+        /// Performs no transformation.
+        /// </summary>
+        /// <remarks>
+        /// Recommended for servers that do not require any character to be escaped or enclosed in quotes,
+        /// or when paths are guaranteed to never contain any special characters (such as #, &quot;, ', $, ...).
+        /// </remarks>
+        public static IRemotePathTransformation None
+        {
+            get { return NoneTransformation; }
+        }
+
+        /// <summary>
+        /// Encloses a path in double quotes, and escapes any embedded double quote with a backslash.
+        /// </summary>
+        /// <value>
+        /// A transformation that encloses a path in double quotes, and escapes any embedded double quote with
+        /// a backslash.
+        /// </value>
+        /// <example>
+        /// <list type="table">
+        ///   <listheader>
+        ///     <term>Original</term>
+        ///     <term>Transformed</term>
+        ///   </listheader>
+        ///   <item>
+        ///     <term>/var/log/auth.log</term>
+        ///     <term>&quot;/var/log/auth.log&quot;</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/mp3/Guns N' Roses</term>
+        ///     <term>&quot;/var/mp3/Guns N' Roses&quot;</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/garbage!/temp</term>
+        ///     <term>&quot;/var/garbage!/temp&quot;</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/would be 'kewl'!, not?</term>
+        ///     <term>&quot;/var/would be 'kewl'!, not?&quot;</term>
+        ///   </item>
+        ///   <item>
+        ///     <term></term>
+        ///     <term>&quot;&quot;</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>Hello &quot;World&quot;</term>
+        ///     <term>&quot;Hello \&quot;World&quot;</term>
+        ///   </item>
+        /// </list>
+        /// </example>
+        public static IRemotePathTransformation DoubleQuote
+        {
+            get { return DoubleQuoteTransformation; }
+        }
+    }
+}

+ 5 - 0
src/Renci.SshNet/Renci.SshNet.csproj

@@ -153,6 +153,7 @@
     <Compile Include="IClientAuthentication.cs" />
     <Compile Include="IConnectionInfo.cs" />
     <Compile Include="IForwardedPort.cs" />
+    <Compile Include="IRemotePathTransformation.cs" />
     <Compile Include="IServiceFactory.cs" />
     <Compile Include="IServiceFactory.NET.cs" />
     <Compile Include="ISession.cs" />
@@ -162,6 +163,10 @@
     <Compile Include="Messages\Transport\KeyExchangeEcdhInitMessage.cs" />
     <Compile Include="Messages\Transport\KeyExchangeEcdhReplyMessage.cs" />
     <Compile Include="Netconf\INetConfSession.cs" />
+    <Compile Include="RemotePathDoubleQuoteTransformation.cs" />
+    <Compile Include="RemotePathNoneTransformation.cs" />
+    <Compile Include="RemotePathShellQuoteTransformation.cs" />
+    <Compile Include="RemotePathTransformation.cs" />
     <Compile Include="Security\Cryptography\HMACMD5.cs" />
     <Compile Include="Security\Cryptography\HMACSHA1.cs" />
     <Compile Include="Security\Cryptography\HMACSHA256.cs" />

+ 4 - 4
src/Renci.SshNet/ScpClient.NET.cs

@@ -36,7 +36,7 @@ namespace Renci.SshNet
                 channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
                 channel.Open();
 
-                if (!channel.SendExecRequest(string.Format("scp -t {0}", path.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -t {0}", _remotePathTransformation.Transform(path))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }
@@ -74,7 +74,7 @@ namespace Renci.SshNet
                 channel.Open();
 
                 // start recursive upload
-                if (!channel.SendExecRequest(string.Format("scp -rt {0}", path.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -rt {0}", _remotePathTransformation.Transform(path))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }
@@ -109,7 +109,7 @@ namespace Renci.SshNet
                 channel.Open();
 
                 // Send channel command request
-                if (!channel.SendExecRequest(string.Format("scp -pf {0}", filename.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -pf {0}", _remotePathTransformation.Transform(filename))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }
@@ -143,7 +143,7 @@ namespace Renci.SshNet
                 channel.Open();
 
                 // Send channel command request
-                if (!channel.SendExecRequest(string.Format("scp -prf {0}", directoryName.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -prf {0}", _remotePathTransformation.Transform(directoryName))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }

+ 33 - 2
src/Renci.SshNet/ScpClient.cs

@@ -33,6 +33,8 @@ namespace Renci.SshNet
         private static readonly byte[] SuccessConfirmationCode = {0};
         private static readonly byte[] ErrorConfirmationCode = { 1 };
 
+        private IRemotePathTransformation _remotePathTransformation;
+
         /// <summary>
         /// Gets or sets the operation timeout.
         /// </summary>
@@ -50,6 +52,34 @@ namespace Renci.SshNet
         /// </value>
         public uint BufferSize { get; set; }
 
+        /// <summary>
+        /// Gets or sets the transformation to apply to remote paths.
+        /// </summary>
+        /// <value>
+        /// The transformation to apply to remote paths. The default is <see cref="SshNet.RemotePathTransformation.DoubleQuote"/>.
+        /// </value>
+        /// <exception cref="ArgumentNullException"><paramref name="value"/> is <c>null</c>.</exception>
+        /// <remarks>
+        /// <para>
+        /// This transformation is applied to the remote file or directory path that is passed to the
+        /// <c>scp</c> command.
+        /// </para>
+        /// <para>
+        /// See <see cref="SshNet.RemotePathTransformation"/> for the transformations that are supplied
+        /// out-of-the-box with SSH.NET.
+        /// </para>
+        /// </remarks>
+        public IRemotePathTransformation RemotePathTransformation
+        {
+            get { return _remotePathTransformation; }
+            set
+            {
+                if (value == null)
+                    throw new ArgumentNullException("value");
+                _remotePathTransformation = value;
+            }
+        }
+
         /// <summary>
         /// Occurs when downloading file.
         /// </summary>
@@ -162,6 +192,7 @@ namespace Renci.SshNet
         {
             OperationTimeout = SshNet.Session.InfiniteTimeSpan;
             BufferSize = 1024 * 16;
+            _remotePathTransformation = serviceFactory.CreateRemotePathDoubleQuoteTransformation();
         }
 
         #endregion
@@ -183,7 +214,7 @@ namespace Renci.SshNet
 
                 // pass the full path to ensure the server does not create the directory part
                 // as a file in case the directory does not exist
-                if (!channel.SendExecRequest(string.Format("scp -t {0}", path.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -t {0}", _remotePathTransformation.Transform(path))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }
@@ -220,7 +251,7 @@ namespace Renci.SshNet
                 channel.Open();
 
                 //  Send channel command request
-                if (!channel.SendExecRequest(string.Format("scp -f {0}", filename.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -f {0}", _remotePathTransformation.Transform(filename))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }

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

@@ -136,5 +136,19 @@ namespace Renci.SshNet
         {
             return new SftpResponseFactory();
         }
+
+        /// <summary>
+        /// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
+        /// any embedded double quote with a backslash.
+        /// </summary>
+        /// <returns>
+        /// An <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes any
+        /// embedded double quote with a backslash.
+        /// with a shell.
+        /// </returns>
+        public IRemotePathTransformation CreateRemotePathDoubleQuoteTransformation()
+        {
+            return RemotePathTransformation.DoubleQuote;
+        }
     }
 }