diff --git a/src/Renci.SshNet/Sftp/Responses/SftpNameResponse.cs b/src/Renci.SshNet/Sftp/Responses/SftpNameResponse.cs index 0e6c9f464..d819fa26f 100644 --- a/src/Renci.SshNet/Sftp/Responses/SftpNameResponse.cs +++ b/src/Renci.SshNet/Sftp/Responses/SftpNameResponse.cs @@ -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(fileName, ReadAttributes()); + var attributes = ReadAttributes(); + attributes.LongName = longName; + Files[i] = new KeyValuePair(fileName, attributes); } } @@ -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 diff --git a/src/Renci.SshNet/Sftp/SftpFile.cs b/src/Renci.SshNet/Sftp/SftpFile.cs index 31cb00023..c1c0df8ea 100644 --- a/src/Renci.SshNet/Sftp/SftpFile.cs +++ b/src/Renci.SshNet/Sftp/SftpFile.cs @@ -19,6 +19,24 @@ public sealed class SftpFile : ISftpFile /// public SftpFileAttributes Attributes { get; private set; } + /// + /// Gets the "longname" that the server sent for this file in a directory listing. + /// + /// + /// A line in the style of the output of ls -l, such as + /// -rw-r--r-- 1 owner group 1024 Oct 9 14:31 file.txt, which servers commonly use to show + /// the names of the owner and the group; or if the server did not send one (SFTP + /// protocol version 4 and later) or if this instance was not obtained from a directory listing. + /// + /// + /// The SFTP protocol does not specify the format of this line: it is meant to be displayed to users. Use + /// for the attributes of the file. + /// + public string LongName + { + get { return Attributes.LongName; } + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Renci.SshNet/Sftp/SftpFileAttributes.cs b/src/Renci.SshNet/Sftp/SftpFileAttributes.cs index 70ca0ddca..158fbd518 100644 --- a/src/Renci.SshNet/Sftp/SftpFileAttributes.cs +++ b/src/Renci.SshNet/Sftp/SftpFileAttributes.cs @@ -570,6 +570,16 @@ public bool OthersCanExecute internal uint Permissions { get; private set; } + /// + /// Gets or sets the "longname" that the server sent with these attributes in an SSH_FXP_NAME response, or + /// if it sent none. + /// + /// + /// It is not part of the attributes on the wire: it is kept here so that it reaches the + /// created from a directory listing. + /// + internal string? LongName { get; set; } + internal SftpFileAttributes(DateTime lastAccessTimeUtc, DateTime lastWriteTimeUtc, long size, int userId, int groupId, uint permissions, Dictionary? extensions) { LastAccessTimeUtc = _originalLastAccessTimeUtc = lastAccessTimeUtc; diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/Responses/SftpNameResponseTest.cs b/test/Renci.SshNet.Tests/Classes/Sftp/Responses/SftpNameResponseTest.cs new file mode 100644 index 000000000..8723acee9 --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/Sftp/Responses/SftpNameResponseTest.cs @@ -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("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); + } + } +} diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/SftpFileTest.cs b/test/Renci.SshNet.Tests/Classes/Sftp/SftpFileTest.cs new file mode 100644 index 000000000..3c2e1049c --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/Sftp/SftpFileTest.cs @@ -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(MockBehavior.Strict).Object, "/home/owner/dir", attributes); + + Assert.AreEqual(longName, target.LongName); + } + + [TestMethod] + public void LongName_WithoutLongNameFromTheServer_IsNull() + { + var target = new SftpFile(new Mock(MockBehavior.Strict).Object, "/home/owner/file.txt", SftpFileAttributesBuilder.Empty); + + Assert.IsNull(target.LongName); + } + } +}