Skip to content

Commit 6184a0e

Browse files
committed
ScpClient: Implement proper quoting of paths.
Fixes issue #256.
1 parent 522ed12 commit 6184a0e

File tree

6 files changed

+318
-19
lines changed

6 files changed

+318
-19
lines changed

src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj

+22-1
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,27 @@
279279
<Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExceptionEventArgsTest.cs">
280280
<Link>Classes\Common\ExceptionEventArgsTest.cs</Link>
281281
</Compile>
282+
<Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_Concat.cs">
283+
<Link>Classes\Common\ExtensionsTest_Concat.cs</Link>
284+
</Compile>
285+
<Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_IsEqualTo_ByteArray.cs">
286+
<Link>Classes\Common\ExtensionsTest_IsEqualTo_ByteArray.cs</Link>
287+
</Compile>
288+
<Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_Reverse.cs">
289+
<Link>Classes\Common\ExtensionsTest_Reverse.cs</Link>
290+
</Compile>
291+
<Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_ShellQuote.cs">
292+
<Link>Classes\Common\ExtensionsTest_ShellQuote.cs</Link>
293+
</Compile>
294+
<Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_Take_Count.cs">
295+
<Link>Classes\Common\ExtensionsTest_Take_Count.cs</Link>
296+
</Compile>
297+
<Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_Take_OffsetAndCount.cs">
298+
<Link>Classes\Common\ExtensionsTest_Take_OffsetAndCount.cs</Link>
299+
</Compile>
300+
<Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_TrimLeadingZeros.cs">
301+
<Link>Classes\Common\ExtensionsTest_TrimLeadingZeros.cs</Link>
302+
</Compile>
282303
<Compile Include="..\Renci.SshNet.Tests\Classes\Common\HostKeyEventArgsTest.cs">
283304
<Link>Classes\Common\HostKeyEventArgsTest.cs</Link>
284305
</Compile>
@@ -1548,7 +1569,7 @@
15481569
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
15491570
<ProjectExtensions>
15501571
<VisualStudio>
1551-
<UserProperties ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
1572+
<UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" />
15521573
</VisualStudio>
15531574
</ProjectExtensions>
15541575
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2+
using System;
3+
4+
namespace Renci.SshNet.Tests.Classes.Common
5+
{
6+
[TestClass]
7+
public class ExtensionsTest_ShellQuote
8+
{
9+
[TestMethod]
10+
public void Null()
11+
{
12+
const string value = null;
13+
14+
try
15+
{
16+
value.ShellQuote();
17+
Assert.Fail();
18+
}
19+
catch (ArgumentNullException ex)
20+
{
21+
Assert.IsNull(ex.InnerException);
22+
Assert.AreEqual("value", ex.ParamName);
23+
}
24+
}
25+
26+
[TestMethod]
27+
public void Empty()
28+
{
29+
var value = string.Empty;
30+
31+
var actual = value.ShellQuote();
32+
33+
Assert.AreEqual("''", actual);
34+
}
35+
36+
[TestMethod]
37+
public void RegularCharacters()
38+
{
39+
var value = "onetwo";
40+
41+
var actual = value.ShellQuote();
42+
43+
Assert.AreEqual("'onetwo'", actual);
44+
}
45+
46+
/// <summary>
47+
/// Tests all special character listed <a href="http://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html">here</a>
48+
/// except for newline and single-quote, which are tested separately.
49+
/// </summary>
50+
[TestMethod]
51+
public void SpecialCharacters()
52+
{
53+
var value = "|&;<>()$`\\\" \t\n*?[#~=%";
54+
55+
var actual = value.ShellQuote();
56+
57+
Assert.AreEqual("'|&;<>()$`\\\" \t\n*?[#~=%'", actual);
58+
}
59+
60+
[TestMethod]
61+
public void SingleExclamationPoint()
62+
{
63+
var value = "!one!two!";
64+
65+
var actual = value.ShellQuote();
66+
67+
Assert.AreEqual("\\!'one'\\!'two'\\!", actual);
68+
}
69+
70+
[TestMethod]
71+
public void SequenceOfExclamationPoints()
72+
{
73+
var value = "one!!!two";
74+
75+
var actual = value.ShellQuote();
76+
77+
Assert.AreEqual("'one'\\!\\!\\!'two'", actual);
78+
}
79+
80+
[TestMethod]
81+
public void SingleQuotes()
82+
{
83+
var value = "'a'b'c'd'";
84+
85+
var actual = value.ShellQuote();
86+
87+
Assert.AreEqual("\"'\"'a'\"'\"'b'\"'\"'c'\"'\"'d'\"'\"", actual);
88+
}
89+
90+
[TestMethod]
91+
public void SequenceOfSingleQuotes()
92+
{
93+
var value = "one''two";
94+
95+
var actual = value.ShellQuote();
96+
97+
Assert.AreEqual("'one'\"''\"'two'", actual);
98+
}
99+
100+
[TestMethod]
101+
public void LineFeeds()
102+
{
103+
var value = "one\ntwo\nthree\nfour";
104+
105+
var actual = value.ShellQuote();
106+
107+
Assert.AreEqual("'one\ntwo\nthree\nfour'", actual);
108+
}
109+
110+
[TestMethod]
111+
public void SequenceOfLineFeeds()
112+
{
113+
var value = "one\n\ntwo";
114+
115+
var actual = value.ShellQuote();
116+
117+
Assert.AreEqual("'one\n\ntwo'", actual);
118+
}
119+
}
120+
}

src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
<Compile Include="Classes\Common\ExtensionsTest_Concat.cs" />
148148
<Compile Include="Classes\Common\ExtensionsTest_IsEqualTo_ByteArray.cs" />
149149
<Compile Include="Classes\Common\ExtensionsTest_Reverse.cs" />
150+
<Compile Include="Classes\Common\ExtensionsTest_ShellQuote.cs" />
150151
<Compile Include="Classes\Common\ExtensionsTest_Take_Count.cs" />
151152
<Compile Include="Classes\Common\ExtensionsTest_Take_OffsetAndCount.cs" />
152153
<Compile Include="Classes\Common\ExtensionsTest_TrimLeadingZeros.cs" />

src/Renci.SshNet/Common/Extensions.cs

+148-2
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,158 @@ namespace Renci.SshNet
1616
/// </summary>
1717
internal static partial class Extensions
1818
{
19+
private enum ShellQuoteState
20+
{
21+
Unquoted = 1,
22+
SingleQuoted = 2,
23+
Quoted = 3
24+
}
25+
26+
/// <summary>
27+
/// Quotes a <see cref="string"/> in a way to be suitable to be used with a shell.
28+
/// </summary>
29+
/// <param name="value">The <see cref="string"/> to quote.</param>
30+
/// <returns>
31+
/// A quoted <see cref="string"/>.
32+
/// </returns>
33+
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <c>null</c>.</exception>
34+
/// <remarks>
35+
/// <para>
36+
/// If <paramref name="value"/> contains a single-quote, that character is embedded
37+
/// in quotation marks (eg. "'"). Sequences of single-quotes are grouped in a one
38+
/// pair of quotation marks.
39+
/// </para>
40+
/// <para>
41+
/// If the <see cref="string"/> contains an exclamation mark (!), the C-Shell interprets
42+
/// it as a meta-character for history substitution. This even works inside single-quotes
43+
/// or quotation marks, unless escaped with a backslash (\).
44+
/// </para>
45+
/// <para>
46+
/// References:
47+
/// <list type="bullet">
48+
/// <item>
49+
/// <description><a href="http://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html">Shell Command Language</a></description>
50+
/// </item>
51+
/// <item>
52+
/// <description><a href="https://earthsci.stanford.edu/computing/unix/shell/specialchars.php">Unix C-Shell special characters and their uses</a></description>
53+
/// </item>
54+
/// <item>
55+
/// <description><a href="https://docstore.mik.ua/orelly/unix3/upt/ch27_13.htm">Differences Between Bourne and C Shell Quoting</a></description>
56+
/// </item>
57+
/// </list>
58+
/// </para>
59+
/// </remarks>
60+
public static string ShellQuote(this string value)
61+
{
62+
if (value == null)
63+
{
64+
throw new ArgumentNullException("value");
65+
}
66+
67+
// result is at least value and leading/trailing single-quote
68+
var sb = new StringBuilder(value.Length + 2);
69+
var state = ShellQuoteState.Unquoted;
70+
71+
foreach (var c in value)
72+
{
73+
switch (c)
74+
{
75+
case '\'':
76+
// embed a single-quote in quotes
77+
switch (state)
78+
{
79+
case ShellQuoteState.Unquoted:
80+
// Start quoted string
81+
sb.Append('"');
82+
break;
83+
case ShellQuoteState.Quoted:
84+
// Continue quoted string
85+
break;
86+
case ShellQuoteState.SingleQuoted:
87+
// Close single quoted string
88+
sb.Append('\'');
89+
// Start quoted string
90+
sb.Append('"');
91+
break;
92+
}
93+
state = ShellQuoteState.Quoted;
94+
break;
95+
case '!':
96+
// In C-Shell, an exclamatation point can only be protected from shell interpretation
97+
// when escaped by a backslash
98+
// Source:
99+
// https://earthsci.stanford.edu/computing/unix/shell/specialchars.php
100+
101+
switch (state)
102+
{
103+
case ShellQuoteState.Unquoted:
104+
sb.Append('\\');
105+
break;
106+
case ShellQuoteState.Quoted:
107+
// Close quoted string
108+
sb.Append('"');
109+
sb.Append('\\');
110+
break;
111+
case ShellQuoteState.SingleQuoted:
112+
// Close single quoted string
113+
sb.Append('\'');
114+
sb.Append('\\');
115+
break;
116+
}
117+
state = ShellQuoteState.Unquoted;
118+
break;
119+
default:
120+
switch (state)
121+
{
122+
case ShellQuoteState.Unquoted:
123+
// Start single-quoted string
124+
sb.Append('\'');
125+
break;
126+
case ShellQuoteState.Quoted:
127+
// Close quoted string
128+
sb.Append('"');
129+
// Start single quoted string
130+
sb.Append('\'');
131+
break;
132+
case ShellQuoteState.SingleQuoted:
133+
// Continue single quoted string
134+
break;
135+
}
136+
state = ShellQuoteState.SingleQuoted;
137+
break;
138+
}
139+
140+
sb.Append(c);
141+
}
142+
143+
switch (state)
144+
{
145+
case ShellQuoteState.Unquoted:
146+
break;
147+
case ShellQuoteState.Quoted:
148+
// Close quoted string
149+
sb.Append('"');
150+
break;
151+
case ShellQuoteState.SingleQuoted:
152+
/* Close single quoted string */
153+
sb.Append('\'');
154+
break;
155+
}
156+
157+
if (sb.Length == 0)
158+
{
159+
sb.Append("''");
160+
}
161+
162+
return sb.ToString();
163+
}
164+
19165
/// <summary>
20-
/// Determines whether [is null or white space] [the specified value].
166+
/// Determines whether the specified value is null or white space.
21167
/// </summary>
22168
/// <param name="value">The value.</param>
23169
/// <returns>
24-
/// <c>true</c> if [is null or white space] [the specified value]; otherwise, <c>false</c>.
170+
/// <c>true</c> if <paramref name="value"/> is null or white space; otherwise, <c>false</c>.
25171
/// </returns>
26172
public static bool IsNullOrWhiteSpace(this string value)
27173
{

0 commit comments

Comments
 (0)