Files
Rob Hague af279d206a Fix SftpFileAttributes file type detection (#1688)
* Fix SftpFileAttributes file type detection

To get the file type, S_IFMT should be used as the mask. Instead it was using each file
type as the mask. It meant that e.g. a symbolic link would also show as a regular file
and a character device.

Also allow setting and retrieving the setuid/setgid/sticky bits

* fix build
2025-10-01 22:19:42 +02:00

224 lines
9.8 KiB
C#

using System;
using System.Buffers.Binary;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Renci.SshNet.Common;
using Renci.SshNet.Sftp;
namespace Renci.SshNet.Tests.Classes.Sftp
{
[TestClass]
public class SftpFileAttributesTest
{
[TestMethod]
[DataRow(0xC000u, true, false, false, false, false, false, false)] // Socket
[DataRow(0xA000u, false, true, false, false, false, false, false)] // Symbolic link
[DataRow(0x8000u, false, false, true, false, false, false, false)] // Regular file
[DataRow(0x6000u, false, false, false, true, false, false, false)] // Block device
[DataRow(0x4000u, false, false, false, false, true, false, false)] // Directory
[DataRow(0x2000u, false, false, false, false, false, true, false)] // Character device
[DataRow(0x1000u, false, false, false, false, false, false, true)] // Named pipe
public void FileTypePropertiesAreMutuallyExclusive(
uint permissions,
bool isSocket,
bool isSymbolicLink,
bool isRegularFile,
bool isBlockDevice,
bool isDirectory,
bool isCharacterDevice,
bool isNamedPipe)
{
var attributeBytes = new byte[8];
attributeBytes[3] = 0x4; // SSH_FILEXFER_ATTR_PERMISSIONS
BinaryPrimitives.WriteUInt32BigEndian(attributeBytes.AsSpan(4), permissions);
var attributes = SftpFileAttributes.FromBytes(attributeBytes);
Assert.AreEqual(isSocket, attributes.IsSocket);
Assert.AreEqual(isSymbolicLink, attributes.IsSymbolicLink);
Assert.AreEqual(isRegularFile, attributes.IsRegularFile);
Assert.AreEqual(isBlockDevice, attributes.IsBlockDevice);
Assert.AreEqual(isDirectory, attributes.IsDirectory);
Assert.AreEqual(isCharacterDevice, attributes.IsCharacterDevice);
Assert.AreEqual(isNamedPipe, attributes.IsNamedPipe);
}
[TestMethod]
public void FromBytesGetBytes()
{
// 81a4 in hex = 100644 in octal
var attributes = SftpFileAttributes.FromBytes([0, 0, 0, 0x4, 0, 0, 0x81, 0xa4]);
Assert.IsTrue(attributes.IsRegularFile);
Assert.IsFalse(attributes.IsUIDBitSet);
Assert.IsFalse(attributes.IsGroupIDBitSet);
Assert.IsFalse(attributes.IsStickyBitSet);
Assert.IsTrue(attributes.OwnerCanRead);
Assert.IsTrue(attributes.OwnerCanWrite);
Assert.IsFalse(attributes.OwnerCanExecute);
Assert.IsTrue(attributes.GroupCanRead);
Assert.IsFalse(attributes.GroupCanWrite);
Assert.IsFalse(attributes.GroupCanExecute);
Assert.IsTrue(attributes.OthersCanRead);
Assert.IsFalse(attributes.OthersCanWrite);
Assert.IsFalse(attributes.OthersCanExecute);
Assert.AreEqual(-1, attributes.Size); // Erm, OK?
Assert.AreEqual(-1, attributes.UserId);
Assert.AreEqual(-1, attributes.GroupId);
Assert.AreEqual(default, attributes.LastAccessTimeUtc);
Assert.AreEqual(DateTimeKind.Utc, attributes.LastAccessTimeUtc.Kind);
Assert.AreEqual(default, attributes.LastWriteTimeUtc);
Assert.AreEqual(DateTimeKind.Utc, attributes.LastWriteTimeUtc.Kind);
Assert.AreEqual("-rw-r--r--", attributes.ToString());
// No changes
CollectionAssert.AreEqual(
new byte[] { 0, 0, 0, 0 },
attributes.GetBytes());
// Permissions change
attributes.IsUIDBitSet = true;
attributes.OwnerCanExecute = true;
CollectionAssert.AreEqual(
new byte[] { 0, 0, 0, 0x4, 0, 0, 0x89, 0xe4 },
attributes.GetBytes());
Assert.AreEqual("-rwsr--r--", attributes.ToString());
// Size change
attributes.Size = 123;
CollectionAssert.AreEqual(
new byte[] {
0, 0, 0, 0x1 | 0x4,
0, 0, 0, 0, 0, 0, 0, 123,
0, 0, 0x89, 0xe4 },
attributes.GetBytes());
Assert.IsTrue(attributes.ToString().StartsWith("-rwsr--r-- Size: ", StringComparison.Ordinal));
// Uid/gid change
attributes.UserId = 99;
attributes.GroupId = 66;
CollectionAssert.AreEqual(
new byte[] {
0, 0, 0, 0x1 | 0x2 | 0x4,
0, 0, 0, 0, 0, 0, 0, 123,
0, 0, 0, 99, 0, 0, 0, 66,
0, 0, 0x89, 0xe4 },
attributes.GetBytes());
// Access/mod time change
attributes.LastAccessTimeUtc = new DateTime(2025, 08, 10, 17, 51, 37, DateTimeKind.Unspecified);
attributes.LastWriteTime = new DateTimeOffset(2016, 12, 02, 13, 18, 20, TimeSpan.FromHours(3)).LocalDateTime;
var expectedTimeBytes = new byte[8];
BinaryPrimitives.WriteUInt32BigEndian(expectedTimeBytes, 1754848297);
BinaryPrimitives.WriteUInt32BigEndian(expectedTimeBytes.AsSpan(4), 1480673900);
CollectionAssert.AreEqual(
new byte[] {
0, 0, 0, 0x1 | 0x2 | 0x4 | 0x8,
0, 0, 0, 0, 0, 0, 0, 123,
0, 0, 0, 99, 0, 0, 0, 66,
0, 0, 0x89, 0xe4
}.Concat(expectedTimeBytes),
attributes.GetBytes());
Assert.AreEqual(new DateTime(2016, 12, 02, 10, 18, 20, DateTimeKind.Utc), attributes.LastWriteTimeUtc);
Assert.AreEqual(DateTimeKind.Utc, attributes.LastWriteTimeUtc.Kind);
var attributesString = attributes.ToString();
Assert.IsTrue(attributesString.StartsWith("-rwsr--r-- Size: ", StringComparison.Ordinal));
Assert.Contains(" LastWriteTime: ", attributesString, StringComparison.CurrentCulture);
}
[TestMethod]
[DataRow((short)8888)]
[DataRow((short)10000)]
[DataRow((short)8000)]
[DataRow((short)0080)]
[DataRow((short)0008)]
[DataRow((short)1797)]
[DataRow((short)-1)]
[DataRow(short.MaxValue)]
public void SetPermissions_InvalidMode_ThrowsArgumentOutOfRangeException(short mode)
{
var attributes = SftpFileAttributes.FromBytes([0, 0, 0, 0]);
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => attributes.SetPermissions(mode));
Assert.AreEqual("mode", ex.ParamName);
}
[TestMethod]
[DataRow((short)0777, false, false, false, true, true, true, true, true, true, true, true, true)]
[DataRow((short)0755, false, false, false, true, true, true, true, false, true, true, false, true)]
[DataRow((short)0644, false, false, false, true, true, false, true, false, false, true, false, false)]
[DataRow((short)0444, false, false, false, true, false, false, true, false, false, true, false, false)]
[DataRow((short)0000, false, false, false, false, false, false, false, false, false, false, false, false)]
[DataRow((short)4700, true, false, false, true, true, true, false, false, false, false, false, false)]
[DataRow((short)3001, false, true, true, false, false, false, false, false, false, false, false, true)]
[DataRow((short)7777, true, true, true, true, true, true, true, true, true, true, true, true)]
public void SetPermissions_ValidMode(
short mode,
bool setUid, bool setGid, bool sticky,
bool ownerRead, bool ownerWrite, bool ownerExec,
bool groupRead, bool groupWrite, bool groupExec,
bool othersRead, bool othersWrite, bool othersExec)
{
var attributes = SftpFileAttributes.FromBytes([0, 0, 0, 0]);
attributes.SetPermissions(mode);
Assert.AreEqual(setUid, attributes.IsUIDBitSet);
Assert.AreEqual(setGid, attributes.IsGroupIDBitSet);
Assert.AreEqual(sticky, attributes.IsStickyBitSet);
Assert.AreEqual(ownerRead, attributes.OwnerCanRead);
Assert.AreEqual(ownerWrite, attributes.OwnerCanWrite);
Assert.AreEqual(ownerExec, attributes.OwnerCanExecute);
Assert.AreEqual(groupRead, attributes.GroupCanRead);
Assert.AreEqual(groupWrite, attributes.GroupCanWrite);
Assert.AreEqual(groupExec, attributes.GroupCanExecute);
Assert.AreEqual(othersRead, attributes.OthersCanRead);
Assert.AreEqual(othersWrite, attributes.OthersCanWrite);
Assert.AreEqual(othersExec, attributes.OthersCanExecute);
}
[TestMethod]
[DataRow(0xC000u, (short)1770, "srwxrwx--T")] // Socket
[DataRow(0xA000u, (short)2707, "lrwx--Srwx")] // Symbolic link
[DataRow(0x8000u, (short)4755, "-rwsr-xr-x")] // Regular file
[DataRow(0x8000u, (short)4644, "-rwSr--r--")] // Regular file
[DataRow(0x6000u, (short)2711, "brwx--s--x")] // Block device
[DataRow(0x4000u, (short)1777, "drwxrwxrwt")] // Directory
[DataRow(0x4000u, (short)1776, "drwxrwxrwT")] // Directory
[DataRow(0x2000u, (short)0660, "crw-rw----")] // Character device
[DataRow(0x1000u, (short)0022, "p----w--w-")] // Named pipe
public void ToStringWithPermissions(
uint fileType,
short permissions,
string expected)
{
var attributeBytes = new byte[8];
attributeBytes[3] = 0x4; // SSH_FILEXFER_ATTR_PERMISSIONS
BinaryPrimitives.WriteUInt32BigEndian(attributeBytes.AsSpan(4), fileType);
var attributes = SftpFileAttributes.FromBytes(attributeBytes);
attributes.SetPermissions(permissions);
Assert.AreEqual(expected, attributes.ToString());
}
}
}