Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/Renci.SshNet/Sftp/Responses/SftpNameResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ protected override void LoadData()
for (var i = 0; i < Count; i++)
{
var fileName = ReadString(Encoding);
string longName = null;
if (SupportsLongName(ProtocolVersion))
{
_ = ReadString(Encoding); // skip longname
longName = ReadString(Encoding);
}

Files[i] = new KeyValuePair<string, SftpFileAttributes>(fileName, ReadAttributes());
var attributes = ReadAttributes();
attributes.LongName = longName;
Files[i] = new KeyValuePair<string, SftpFileAttributes>(fileName, attributes);
}
}

Expand All @@ -57,7 +60,7 @@ protected override void SaveData()

if (SupportsLongName(ProtocolVersion))
{
Write(0U); // longname
Write(file.Value.LongName ?? string.Empty, Encoding); // longname
}

Write(file.Value.GetBytes()); // attrs
Expand Down
18 changes: 18 additions & 0 deletions src/Renci.SshNet/Sftp/SftpFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ public sealed class SftpFile : ISftpFile
/// </summary>
public SftpFileAttributes Attributes { get; private set; }

/// <summary>
/// Gets the "longname" that the server sent for this file in a directory listing.
/// </summary>
/// <value>
/// A line in the style of the output of <c>ls -l</c>, such as
/// <c>-rw-r--r-- 1 owner group 1024 Oct 9 14:31 file.txt</c>, which servers commonly use to show
/// the names of the owner and the group; or <see langword="null"/> if the server did not send one (SFTP
/// protocol version 4 and later) or if this instance was not obtained from a directory listing.
/// </value>
/// <remarks>
/// The SFTP protocol does not specify the format of this line: it is meant to be displayed to users. Use
/// <see cref="Attributes"/> for the attributes of the file.
/// </remarks>
public string LongName
{
get { return Attributes.LongName; }
}

/// <summary>
/// Initializes a new instance of the <see cref="SftpFile"/> class.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/Renci.SshNet/Sftp/SftpFileAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,16 @@ public bool OthersCanExecute

internal uint Permissions { get; private set; }

/// <summary>
/// Gets or sets the "longname" that the server sent with these attributes in an SSH_FXP_NAME response, or
/// <see langword="null"/> if it sent none.
/// </summary>
/// <remarks>
/// It is not part of the attributes on the wire: it is kept here so that it reaches the <see cref="SftpFile"/>
/// created from a directory listing.
/// </remarks>
internal string? LongName { get; set; }

internal SftpFileAttributes(DateTime lastAccessTimeUtc, DateTime lastWriteTimeUtc, long size, int userId, int groupId, uint permissions, Dictionary<string, string>? extensions)
{
LastAccessTimeUtc = _originalLastAccessTimeUtc = lastAccessTimeUtc;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Collections.Generic;
using System.Text;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using Renci.SshNet.Common;
using Renci.SshNet.Sftp;
using Renci.SshNet.Sftp.Responses;
using Renci.SshNet.Tests.Common;

namespace Renci.SshNet.Tests.Classes.Sftp.Responses
{
[TestClass]
public class SftpNameResponseTest
{
private const string LongName = "-rw-r--r-- 1 owner group 1024 Oct 9 14:31 file.txt";

[TestMethod]
public void Load_ProtocolVersion3_KeepsTheLongNameOfEachFile()
{
var attributesBytes = SftpFileAttributesBuilder.Empty.GetBytes();
var sshDataStream = new SshDataStream(0);
sshDataStream.Write(1U); // response id
sshDataStream.Write(2U); // count
sshDataStream.Write("file.txt", Encoding.UTF8);
sshDataStream.Write(LongName, Encoding.UTF8);
sshDataStream.Write(attributesBytes);
sshDataStream.Write("other", Encoding.UTF8);
sshDataStream.Write(string.Empty, Encoding.UTF8);
sshDataStream.Write(attributesBytes);

var target = new SftpNameResponse(3, Encoding.UTF8);
target.Load(sshDataStream.ToArray());

Assert.AreEqual(2U, target.Count);
Assert.AreEqual("file.txt", target.Files[0].Key);
Assert.AreEqual(LongName, target.Files[0].Value.LongName);
Assert.AreEqual("other", target.Files[1].Key);
Assert.AreEqual(string.Empty, target.Files[1].Value.LongName);
}

[TestMethod]
public void Load_ProtocolVersion4_HasNoLongName()
{
var attributesBytes = SftpFileAttributesBuilder.Empty.GetBytes();
var sshDataStream = new SshDataStream(0);
sshDataStream.Write(1U); // response id
sshDataStream.Write(1U); // count
sshDataStream.Write("file.txt", Encoding.UTF8);
sshDataStream.Write(attributesBytes);

var target = new SftpNameResponse(4, Encoding.UTF8);
target.Load(sshDataStream.ToArray());

Assert.AreEqual("file.txt", target.Files[0].Key);
Assert.IsNull(target.Files[0].Value.LongName);
}

[TestMethod]
public void GetBytes_ProtocolVersion3_WritesTheLongName()
{
var attributes = SftpFileAttributesBuilder.Empty;
attributes.LongName = LongName;
var target = new SftpNameResponse(3, Encoding.UTF8)
{
ResponseId = 7,
Files = [new KeyValuePair<string, SftpFileAttributes>("file.txt", attributes)],
};

var bytes = target.GetBytes();

// 4 bytes for the length of the SFTP data, 1 byte for the SFTP message type
var loaded = new SftpNameResponse(3, Encoding.UTF8);
loaded.Load(bytes, 5, bytes.Length - 5);
Assert.AreEqual(7U, loaded.ResponseId);
Assert.AreEqual("file.txt", loaded.Files[0].Key);
Assert.AreEqual(LongName, loaded.Files[0].Value.LongName);
}
}
}
33 changes: 33 additions & 0 deletions test/Renci.SshNet.Tests/Classes/Sftp/SftpFileTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Moq;

using Renci.SshNet.Sftp;
using Renci.SshNet.Tests.Common;

namespace Renci.SshNet.Tests.Classes.Sftp
{
[TestClass]
public class SftpFileTest
{
[TestMethod]
public void LongName_FileFromADirectoryListing_IsTheLineSentByTheServer()
{
const string longName = "drwxr-xr-x 3 owner group 1024 Oct 9 14:31 dir";
var attributes = SftpFileAttributesBuilder.Empty;
attributes.LongName = longName;

var target = new SftpFile(new Mock<ISftpSession>(MockBehavior.Strict).Object, "/home/owner/dir", attributes);

Assert.AreEqual(longName, target.LongName);
}

[TestMethod]
public void LongName_WithoutLongNameFromTheServer_IsNull()
{
var target = new SftpFile(new Mock<ISftpSession>(MockBehavior.Strict).Object, "/home/owner/file.txt", SftpFileAttributesBuilder.Empty);

Assert.IsNull(target.LongName);
}
}
}