From fe712f8de21680dfd5894e743951a9371c3c15bd Mon Sep 17 00:00:00 2001 From: "James Truher [MSFT]" Date: Fri, 15 Nov 2019 17:26:11 -0800 Subject: [PATCH] Experimental Feature: Provide Unix stat information in filesystem output (#11042) --- .../CoreCLR/CorePsPlatform.cs | 485 +++++++++++++++++- .../FileSystem_format_ps1xml.cs | 24 + .../System.Management.Automation.csproj | 2 +- .../ExperimentalFeature.cs | 5 + .../engine/TypeTable_Types_Ps1Xml.cs | 47 ++ .../namespaces/ProviderBase.cs | 21 + .../UnixStat.Tests.ps1 | 126 +++++ .../Get-FormatData.Tests.ps1 | 2 +- .../powershell/engine/ETS/TypeTable.Tests.ps1 | 2 +- 9 files changed, 693 insertions(+), 21 deletions(-) create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Management/UnixStat.Tests.ps1 diff --git a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs index 85154cffae..dfa5ff300f 100644 --- a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs +++ b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs @@ -554,27 +554,284 @@ namespace System.Management.Automation return IsMacOS ? Unix.NativeMethods.GetPPid(pid) : Unix.GetProcFSParentPid(pid); } - // Unix specific implementations of required functionality - // // Please note that `Win32Exception(Marshal.GetLastWin32Error())` // works *correctly* on Linux in that it creates an exception with // the string perror would give you for the last set value of errno. // No manual mapping is required. .NET Core maps the Linux errno // to a PAL value and calls strerror_r underneath to generate the message. + + /// Unix specific implementations of required functionality. internal static class Unix { + private static Dictionary usernameCache = new Dictionary(); + private static Dictionary groupnameCache = new Dictionary(); + + /// The type of a Unix file system item. + public enum ItemType + { + /// The item is a Directory. + Directory, + + /// The item is a File. + File, + + /// The item is a Symbolic Link. + SymbolicLink, + + /// The item is a Block Device. + BlockDevice, + + /// The item is a Character Device. + CharacterDevice, + + /// The item is a Named Pipe. + NamedPipe, + + /// The item is a Socket. + Socket, + } + + /// The mask to use to retrieve specific mode bits from the mode value in the stat class. + public enum StatMask + { + /// The mask to collect the owner mode. + OwnerModeMask = 0x1C0, + + /// The mask to get the owners read bit. + OwnerRead = 0x100, + + /// The mask to get the owners write bit. + OwnerWrite = 0x080, + + /// The mask to get the owners execute bit. + OwnerExecute = 0x040, + + /// The mask to get the group mode. + GroupModeMask = 0x038, + + /// The mask to get the group mode. + GroupRead = 0x20, + + /// The mask to get the group mode. + GroupWrite = 0x10, + + /// The mask to get the group mode. + GroupExecute = 0x8, + + /// The mask to get the "other" mode. + OtherModeMask = 0x007, + + /// The mask to get the "other" read bit. + OtherRead = 0x004, + + /// The mask to get the "other" write bit. + OtherWrite = 0x002, + + /// The mask to get the "other" execute bit. + OtherExecute = 0x001, + + /// The mask to retrieve the sticky bit. + SetStickyMask = 0x200, + + /// The mask to retrieve the setgid bit. + SetGidMask = 0x400, + + /// The mask to retrieve the setuid bit. + SetUidMask = 0x800, + } + + /// The Common Stat class. + public class CommonStat + { + /// The inode of the filesystem item. + public long Inode; + + /// The Mode of the filesystem item. + public int Mode; + + /// The user id of the filesystem item. + public int UserId; + + /// The group id of the filesystem item. + public int GroupId; + + /// The number of hard links for the filesystem item. + public int HardlinkCount; + + /// The size in bytes of the filesystem item. + public long Size; + + /// The last access time of the filesystem item. + public DateTime AccessTime; + + /// The last modified time for the filesystem item. + public DateTime ModifiedTime; + + /// The last time the status changes for the filesystem item. + public DateTime StatusChangeTime; + + /// The block size of the filesystem. + public long BlockSize; + + /// The device id of the filesystem item. + public int DeviceId; + + /// The number of blocks used by the filesystem item. + public int NumberOfBlocks; + + /// The type of the filesystem item. + public ItemType ItemType; + + /// Whether the filesystem item has the setuid bit enabled. + public bool IsSetUid; + + /// Whether the filesystem item has the setgid bit enabled. + public bool IsSetGid; + + /// Whether the filesystem item has the sticky bit enabled. This is only available for directories. + public bool IsSticky; + + private const char CanRead = 'r'; + private const char CanWrite = 'w'; + private const char CanExecute = 'x'; + + // helper for getting unix mode + private Dictionary modeMap = new Dictionary() + { + { StatMask.OwnerRead, CanRead }, + { StatMask.OwnerWrite, CanWrite }, + { StatMask.OwnerExecute, CanExecute }, + { StatMask.GroupRead, CanRead }, + { StatMask.GroupWrite, CanWrite }, + { StatMask.GroupExecute, CanExecute }, + { StatMask.OtherRead, CanRead }, + { StatMask.OtherWrite, CanWrite }, + { StatMask.OtherExecute, CanExecute }, + }; + + private StatMask[] permissions = new StatMask[] + { + StatMask.OwnerRead, + StatMask.OwnerWrite, + StatMask.OwnerExecute, + StatMask.GroupRead, + StatMask.GroupWrite, + StatMask.GroupExecute, + StatMask.OtherRead, + StatMask.OtherWrite, + StatMask.OtherExecute + }; + + // The item type and the character representation for the first element in the stat string + private Dictionary itemTypeTable = new Dictionary() + { + { ItemType.BlockDevice, 'b' }, + { ItemType.CharacterDevice, 'c' }, + { ItemType.Directory, 'd' }, + { ItemType.File, '-' }, + { ItemType.NamedPipe, 'p' }, + { ItemType.Socket, 's' }, + { ItemType.SymbolicLink, 'l' }, + }; + + /// Convert the mode to a string which is usable in our formatting. + /// The mode converted into a Unix style string similar to the output of ls. + public string GetModeString() + { + int offset = 0; + char[] modeCharacters = new char[10]; + modeCharacters[offset++] = itemTypeTable[ItemType]; + + foreach (StatMask permission in permissions) + { + // determine whether we are setuid, sticky, or the usual rwx. + if ((Mode & (int)permission) == (int)permission) + { + if ((permission == StatMask.OwnerExecute && IsSetUid) || (permission == StatMask.GroupExecute && IsSetGid)) + { + // Check for setuid and add 's' + modeCharacters[offset] = 's'; + } + else if (permission == StatMask.OtherExecute && IsSticky && (ItemType == ItemType.Directory)) + { + // Directories are sticky, rather than setuid + modeCharacters[offset] = 't'; + } + else + { + modeCharacters[offset] = modeMap[permission]; + } + } + else + { + modeCharacters[offset] = '-'; + } + + offset++; + } + + return new string(modeCharacters); + } + + /// + /// Get the user name. This is used in formatting, but we shouldn't + /// do the pinvoke this unless we're going to use it. + /// + /// The user name. + public string GetUserName() + { + if (usernameCache.TryGetValue(UserId, out string username)) + { + return username; + } + + // Get and add the user name to the cache so we don't need to + // have a pinvoke for each file. + username = NativeMethods.GetPwUid(UserId); + usernameCache.Add(UserId, username); + + return username; + } + + /// + /// Get the group name. This is used in formatting, but we shouldn't + /// do the pinvoke this unless we're going to use it. + /// + /// The name of the group. + public string GetGroupName() + { + if (groupnameCache.TryGetValue(GroupId, out string groupname)) + { + return groupname; + } + + // Get and add the group name to the cache so we don't need to + // have a pinvoke for each file. + groupname = NativeMethods.GetGrGid(GroupId); + groupnameCache.Add(GroupId, groupname); + + return groupname; + } + } + // This is a helper that attempts to map errno into a PowerShell ErrorCategory internal static ErrorCategory GetErrorCategory(int errno) { return (ErrorCategory)Unix.NativeMethods.GetErrorCategory(errno); } + /// Is this a hardlink. + /// The handle to a file. + /// A boolean that represents whether the item is a hardlink. public static bool IsHardLink(ref IntPtr handle) { // TODO:PSL implement using fstat to query inode refcount to see if it is a hard link return false; } + /// Determine if the item is a hardlink. + /// A FileSystemInfo to check to determine if it is a hardlink. + /// A boolean that represents whether the item is a hardlink. public static bool IsHardLink(FileSystemInfo fs) { if (!fs.Exists || (fs.Attributes & FileAttributes.Directory) == FileAttributes.Directory) @@ -589,19 +846,105 @@ namespace System.Management.Automation { return count > 1; } - else - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } + + throw new Win32Exception(Marshal.GetLastWin32Error()); } + /// + /// Create a managed replica of the native stat structure. + /// + /// The common stat structure from which we copy. + /// A managed common stat class instance. + private static CommonStat CopyStatStruct(NativeMethods.CommonStatStruct css) + { + CommonStat cs = new CommonStat(); + cs.Inode = css.Inode; + cs.Mode = css.Mode; + cs.UserId = css.UserId; + cs.GroupId = css.GroupId; + cs.HardlinkCount = css.HardlinkCount; + cs.Size = css.Size; + cs.AccessTime = DateTime.UnixEpoch.AddSeconds(css.AccessTime).ToLocalTime(); + cs.ModifiedTime = DateTime.UnixEpoch.AddSeconds(css.ModifiedTime).ToLocalTime(); + cs.StatusChangeTime = DateTime.UnixEpoch.AddSeconds(css.StatusChangeTime).ToLocalTime(); + cs.BlockSize = css.BlockSize; + cs.DeviceId = css.DeviceId; + cs.NumberOfBlocks = css.NumberOfBlocks; + + if (css.IsDirectory == 1) + { + cs.ItemType = ItemType.Directory; + } + else if (css.IsFile == 1) + { + cs.ItemType = ItemType.File; + } + else if (css.IsSymbolicLink == 1) + { + cs.ItemType = ItemType.SymbolicLink; + } + else if (css.IsBlockDevice == 1) + { + cs.ItemType = ItemType.BlockDevice; + } + else if (css.IsCharacterDevice == 1) + { + cs.ItemType = ItemType.CharacterDevice; + } + else if (css.IsNamedPipe == 1) + { + cs.ItemType = ItemType.NamedPipe; + } + else + { + cs.ItemType = ItemType.Socket; + } + + cs.IsSetUid = css.IsSetUid == 1; + cs.IsSetGid = css.IsSetGid == 1; + cs.IsSticky = css.IsSticky == 1; + + return cs; + } + + /// Get the lstat info from a path. + /// The path to the lstat information. + /// An instance of the CommonStat for the path. + public static CommonStat GetLStat(string path) + { + NativeMethods.CommonStatStruct css; + if (NativeMethods.GetCommonLStat(path, out css) == 0) + { + return CopyStatStruct(css); + } + + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + /// Get the stat info from a path. + /// The path to the stat information. + /// An instance of the CommonStat for the path. + public static CommonStat GetStat(string path) + { + NativeMethods.CommonStatStruct css; + if (NativeMethods.GetCommonStat(path, out css) == 0) + { + return CopyStatStruct(css); + } + + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + /// Read the /proc file system for information about the parent. + /// The process id used to get the parent process. + /// The process id. public static int GetProcFSParentPid(int pid) { const int invalidPid = -1; + // read /proc//stat // 4th column will contain the ppid, 92 in the example below // ex: 93 (bash) S 92 93 2 4294967295 ... - var path = $"/proc/{pid}/stat"; try { @@ -620,12 +963,12 @@ namespace System.Management.Automation } } + /// The native methods class. internal static class NativeMethods { private const string psLib = "libpsl-native"; // Ansi is a misnomer, it is hardcoded to UTF-8 on Linux and macOS - // C bools are 1 byte and so must be marshaled as I1 [DllImport(psLib, CharSet = CharSet.Ansi)] @@ -644,21 +987,39 @@ namespace System.Management.Automation [DllImport(psLib, CharSet = CharSet.Ansi)] internal static extern uint GetCurrentThreadId(); - // This is a struct tm from + // This is a struct tm from . [StructLayout(LayoutKind.Sequential)] internal unsafe struct UnixTm { - public int tm_sec; /* Seconds (0-60) */ - public int tm_min; /* Minutes (0-59) */ - public int tm_hour; /* Hours (0-23) */ - public int tm_mday; /* Day of the month (1-31) */ - public int tm_mon; /* Month (0-11) */ - public int tm_year; /* Year - 1900 */ - public int tm_wday; /* Day of the week (0-6, Sunday = 0) */ - public int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */ - public int tm_isdst; /* Daylight saving time */ + /// Seconds (0-60). + internal int tm_sec; + + /// Minutes (0-59). + internal int tm_min; + + /// Hours (0-23). + internal int tm_hour; + + /// Day of the month (1-31). + internal int tm_mday; + + /// Month (0-11). + internal int tm_mon; + + /// The year - 1900. + internal int tm_year; + + /// Day of the week (0-6, Sunday = 0). + internal int tm_wday; + + /// Day in the year (0-365, 1 Jan = 0). + internal int tm_yday; + + /// Daylight saving time. + internal int tm_isdst; } + // We need a way to convert a DateTime to a unix date. internal static UnixTm DateTimeToUnixTm(DateTime date) { UnixTm tm; @@ -701,6 +1062,94 @@ namespace System.Management.Automation [DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)] internal static extern int GetInodeData([MarshalAs(UnmanagedType.LPStr)]string path, out UInt64 device, out UInt64 inode); + + /// + /// This is a struct from getcommonstat.h in the native library. + /// It presents each member of the stat structure as the largest type of that member across + /// all stat structures on the platforms we support. This allows us to present a common + /// stat structure for all our platforms. + /// + [StructLayout(LayoutKind.Sequential)] + internal struct CommonStatStruct + { + /// The inode of the filesystem item. + internal long Inode; + + /// The mode of the filesystem item. + internal int Mode; + + /// The user id of the filesystem item. + internal int UserId; + + /// The group id of the filesystem item. + internal int GroupId; + + /// The number of hard links to the filesystem item. + internal int HardlinkCount; + + /// The size in bytes of the filesystem item. + internal long Size; + + /// The time of the last access for the filesystem item. + internal long AccessTime; + + /// The time of the last modification for the filesystem item. + internal long ModifiedTime; + + /// The time of the last status change for the filesystem item. + internal long StatusChangeTime; + + /// The size in bytes of the file system. + internal long BlockSize; + + /// The device id for the filesystem item. + internal int DeviceId; + + /// The number of filesystem blocks that the filesystem item uses. + internal int NumberOfBlocks; + + /// This filesystem item is a directory. + internal int IsDirectory; + + /// This filesystem item is a file. + internal int IsFile; + + /// This filesystem item is a symbolic link. + internal int IsSymbolicLink; + + /// This filesystem item is a block device. + internal int IsBlockDevice; + + /// This filesystem item is a character device. + internal int IsCharacterDevice; + + /// This filesystem item is a named pipe. + internal int IsNamedPipe; + + /// This filesystem item is a socket. + internal int IsSocket; + + /// This filesystem item will run as the the owner if executed. + internal int IsSetUid; + + /// This filesystem item will run as the the group if executed. + internal int IsSetGid; + + /// Whether the sticky bit is set on the filesystem item. + internal int IsSticky; + } + + [DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)] + internal static extern unsafe int GetCommonLStat(string filePath, [Out] out CommonStatStruct cs); + + [DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)] + internal static extern unsafe int GetCommonStat(string filePath, [Out] out CommonStatStruct cs); + + [DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)] + internal static extern string GetPwUid(int id); + + [DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)] + internal static extern string GetGrGid(int id); } } } diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/FileSystem_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/FileSystem_format_ps1xml.cs index b65fde9eb6..0346a09418 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/FileSystem_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/FileSystem_format_ps1xml.cs @@ -41,6 +41,30 @@ namespace System.Management.Automation.Runspaces private static IEnumerable ViewsOf_FileSystemTypes(CustomControl[] sharedControls) { +#if UNIX + if (ExperimentalFeature.IsEnabled("PSUnixFileStat")) + { + yield return new FormatViewDefinition("childrenWithUnixStat", + TableControl.Create() + .GroupByProperty("PSParentPath", customControl: sharedControls[0]) + .AddHeader(Alignment.Left, label: "UnixMode", width: 10) + .AddHeader(Alignment.Left, label: "User", width: 16) + .AddHeader(Alignment.Left, label: "Group", width: 16) + .AddHeader(Alignment.Right, label: "LastWriteTime", width: 18) + .AddHeader(Alignment.Right, label: "Size", width: 14) + .AddHeader(Alignment.Left, label: "Name") + .StartRowDefinition(wrap: true) + .AddPropertyColumn("UnixMode") + .AddPropertyColumn("User") + .AddPropertyColumn("Group") + .AddScriptBlockColumn(scriptBlock: @"'{0:d} {0:HH}:{0:mm}' -f $_.LastWriteTime") + .AddPropertyColumn("Size") + .AddPropertyColumn("NameString") + .EndRowDefinition() + .EndTable()); + } +#endif + yield return new FormatViewDefinition("children", TableControl.Create() .GroupByProperty("PSParentPath", customControl: sharedControls[0]) diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index 75454c62d0..605eb6e700 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -28,7 +28,7 @@ - + diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index 95824a9992..9313e72e7e 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -127,6 +127,11 @@ namespace System.Management.Automation new ExperimentalFeature( name: "PSPipelineChainOperators", description: "Allow use of && and || as operators between pipeline invocations"), +#if UNIX + new ExperimentalFeature( + name: "PSUnixFileStat", + description: "Provide unix permission information for files and directories"), +#endif new ExperimentalFeature( name: "PSNullConditionalOperators", description: "Support the null conditional member access operators in PowerShell language") diff --git a/src/System.Management.Automation/engine/TypeTable_Types_Ps1Xml.cs b/src/System.Management.Automation/engine/TypeTable_Types_Ps1Xml.cs index 257a9b79d3..be3bf66889 100644 --- a/src/System.Management.Automation/engine/TypeTable_Types_Ps1Xml.cs +++ b/src/System.Management.Automation/engine/TypeTable_Types_Ps1Xml.cs @@ -9198,6 +9198,53 @@ namespace System.Management.Automation.Runspaces #endregion System.Version#IncludeLabel +#if UNIX + #region UnixStat + + + if (ExperimentalFeature.IsEnabled("PSUnixFileStat")) + { + typeName = @"System.IO.FileSystemInfo"; + typeMembers = _extendedMembers.GetOrAdd(typeName, GetValueFactoryBasedOnInitCapacity(capacity: 1)); + + // Where we have a method to invoke below, first check to be sure that the object is present + // to avoid null reference issues + newMembers.Add(@"UnixMode"); + AddMember( + errors, + typeName, + new PSScriptProperty(@"UnixMode", GetScriptBlock(@"if ($this.UnixStat) { $this.UnixStat.GetModeString() }")), + typeMembers, + isOverride: false); + + newMembers.Add(@"User"); + AddMember( + errors, + typeName, + new PSScriptProperty(@"User", GetScriptBlock(@" if ($this.UnixStat) { $this.UnixStat.GetUserName() } ")), + typeMembers, + isOverride: false); + + newMembers.Add(@"Group"); + AddMember( + errors, + typeName, + new PSScriptProperty(@"Group", GetScriptBlock(@" if ($this.UnixStat) { $this.UnixStat.GetGroupName() } ")), + typeMembers, + isOverride: false); + + newMembers.Add(@"Size"); + AddMember( + errors, + typeName, + new PSScriptProperty(@"Size", GetScriptBlock(@"$this.UnixStat.Size")), + typeMembers, + isOverride: false); + } + + #endregion +#endif + // Update binder version for newly added members. foreach (string memberName in newMembers) { diff --git a/src/System.Management.Automation/namespaces/ProviderBase.cs b/src/System.Management.Automation/namespaces/ProviderBase.cs index 5d706b10d3..4b9e46ab59 100644 --- a/src/System.Management.Automation/namespaces/ProviderBase.cs +++ b/src/System.Management.Automation/namespaces/ProviderBase.cs @@ -1862,6 +1862,27 @@ namespace System.Management.Automation.Provider result.AddOrSetProperty("PSChildName", childName); providerBaseTracer.WriteLine("Attaching {0} = {1}", "PSChildName", childName); +#if UNIX + + // Add a commonstat structure to file system objects + if (ExperimentalFeature.IsEnabled("PSUnixFileStat") && ProviderInfo.ImplementingType == typeof(Microsoft.PowerShell.Commands.FileSystemProvider)) + { + try + { + // Use LStat because if you get a link, you want the information about the + // link, not the file. + var commonStat = Platform.Unix.GetLStat(path); + result.AddOrSetProperty("UnixStat", commonStat); + } + catch + { + // If there is *any* problem in retrieving the stat information + // set the property to null. There is no specific exception which + // would result in different behavior. + result.AddOrSetProperty("UnixStat", value: null); + } + } +#endif } // PSDriveInfo diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/UnixStat.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/UnixStat.Tests.ps1 new file mode 100644 index 0000000000..c6df456615 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/UnixStat.Tests.ps1 @@ -0,0 +1,126 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +Describe "UnixFileSystem additions" -Tag "CI" { + # if PSUnixFileStat is converted from an experimental feature, these tests will need to be changed + BeforeAll { + $experimentalFeatureName = "PSUnixFileStat" + $skipTest = -not $EnabledExperimentalFeatures.Contains($experimentalFeatureName) + $PSDefaultParameterValues.Add('It:Skip', $skipTest) + } + AfterAll { + $PSDefaultParameterValues.Remove('It:Skip') + } + Context "Basic Validation" { + + It "Should be an experimental feature on non-Windows systems" { + $feature = Get-ExperimentalFeature -Name $experimentalFeatureName + if ( $IsWindows ) { + $feature | Should -BeNullOrEmpty + } + else { + $feature.Name | Should -Be $experimentalFeatureName + } + } + + It "Should include a UnixStat property" { + $i = Get-Item ${TestDrive} + $i.UnixStat | Should -Not -BeNullOrEmpty + } + + It "The UnixStat property should be the correct type" { + $expected = "System.Management.Automation.Platform+Unix+CommonStat" + $i = (get-item /).psobject.properties['UnixStat'].TypeNameOfValue + $i | Should -Be $expected + } + } + + Context "Validation of additional properties on file system objects" { + BeforeAll { + if ( $IsWindows ) { + return + } + + $testDir = "${TestDrive}/TestDir" + $testFile = "${testDir}/TestFile" + + $testCase = @{ Mode = '000'; Perm = '----------'; Item = "${testFile}" }, + @{ Mode = '111'; Perm = '---x--x--x'; Item = "${testFile}" }, + @{ Mode = '222'; Perm = '--w--w--w-'; Item = "${testFile}" }, + @{ Mode = '333'; Perm = '--wx-wx-wx'; Item = "${testFile}" }, + @{ Mode = '444'; Perm = '-r--r--r--'; Item = "${testFile}" }, + @{ Mode = '555'; Perm = '-r-xr-xr-x'; Item = "${testFile}" }, + @{ Mode = '666'; Perm = '-rw-rw-rw-'; Item = "${testFile}" }, + @{ Mode = '777'; Perm = '-rwxrwxrwx'; Item = "${testFile}" }, + @{ Mode = '4777'; Perm = '-rwsrwxrwx'; Item = "${testFile}" }, + @{ Mode = '1777'; Perm = 'drwxrwxrwt'; Item = "${testDir}" } + } + + BeforeEach { + $null = New-Item -ItemType Directory -Path "${testDir}" + $null = New-Item -ItemType File -Path "${testFile}" + } + + AfterEach { + Remove-Item -Path "${testFile}" -Force + Remove-Item -Path "${testDir}" -Recurse -Force + } + + It "Should present filemode '' string correctly as ''" -testCase $testCase { + param ($Mode, $Perm, $Item ) + chmod "$Mode" "${Item}" + $i = Get-Item $Item + $i.UnixMode | Should -Be $Perm + } + + It "Should retrieve the user name for the file" { + $i = Get-Item ${testFile} + $user = (/bin/ls -ld $testFile).split(" ",[System.StringSplitOptions]"RemoveEmptyEntries")[2] + $i.User | Should -Be $user + } + + It "Should retrieve the group name for the file" { + $i = Get-Item ${testFile} + $group = (/bin/ls -ld $testFile).split(" ",[System.StringSplitOptions]"RemoveEmptyEntries")[3] + $i.Group | Should -Be $Group + } + } + + Context "Other properties of UnixStat object" { + BeforeAll { + if ( $IsWindows ) { + return + } + + $testDir = "${TestDrive}/TestDir" + $testFile = "${testDir}/TestFile" + + $null = New-Item -Type Directory -Path $testDir + $null = New-Item -Type File -Path $testFile + Set-Content -Path ${testFile} -Value "abc" + + $expectedFileInode,$permission,$expectedFileLinkCount,$user,$group,$expectedFileSize,$unused = + (/bin/ls -ldi ${testFile}).Split(" ", 7, [System.StringSplitOptions]"RemoveEmptyEntries") + $file = Get-Item ${testFile} + + $expectedDirInode,$permission,$expectedDirLinkCount,$user,$group,$expectedDirSize,$unused = + (/bin/ls -ldi ${testDir}).Split(" ", 7, [System.StringSplitOptions]"RemoveEmptyEntries") + $Dir = Get-Item ${testDir} + + $testCases = + @{ Expected = $expectedFileInode; Observed = $File.UnixStat.Inode; Title = "FileInode" }, + @{ Expected = $expectedFileLinkCount; Observed = $File.UnixStat.HardlinkCount; Title = "FileHardlinkCount" }, + @{ Expected = $expectedFileSize; Observed = $File.UnixStat.Size; Title = "FileSize" }, + @{ Expected = $expectedDirInode; Observed = $Dir.UnixStat.Inode; Title = "DirInode" }, + @{ Expected = $expectedDirLinkCount; Observed = $Dir.UnixStat.HardlinkCount; Title = "DirHardlinkCount" }, + @{ Expected = $expectedDirSize; Observed = $Dir.UnixStat.Size; Title = "DirSize" } + } + + It "Should have correct values in UnixStat property for ''" -TestCases $testCases { + param ( $Title, $expected, $observed ) + + $observed | Should -Be $expected + + } + } + +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-FormatData.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-FormatData.Tests.ps1 index 5c50e448f1..e4f71441bc 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-FormatData.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-FormatData.Tests.ps1 @@ -15,7 +15,7 @@ Describe "Get-FormatData" -Tags "CI" { $format.TypeNames | Should -HaveCount 2 $format.TypeNames[0] | Should -BeExactly "System.IO.DirectoryInfo" $format.TypeNames[1] | Should -BeExactly "System.IO.FileInfo" - $format.FormatViewDefinition | Should -HaveCount 4 + $format.FormatViewDefinition | Should -HaveCount ($IsWindows ? 4 : 5) } It "Should return nothing for format data requiring '-PowerShellVersion 5.1' and not provided" { diff --git a/test/powershell/engine/ETS/TypeTable.Tests.ps1 b/test/powershell/engine/ETS/TypeTable.Tests.ps1 index deccc8c4a3..4b8fd759c4 100644 --- a/test/powershell/engine/ETS/TypeTable.Tests.ps1 +++ b/test/powershell/engine/ETS/TypeTable.Tests.ps1 @@ -18,7 +18,7 @@ Describe "Built-in type information tests" -Tag "CI" { } It "Should have correct number of built-in type items in type table" { - $types.Count | Should -BeExactly ($IsWindows ? 273 : 271) + $types.Count | Should -BeExactly ($IsWindows ? 273 : 272) } It "Should have expected member info for 'System.Diagnostics.ProcessModule'" {