From aff018234c70ba4d4c15f3688f71576f1d830ead Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 10 Aug 2020 17:43:07 -0700 Subject: [PATCH 1/4] Ensure FileStatus and FileSystemEntry IsHidden attribute is retrieved the same way --- .../IO/Enumeration/FileSystemEntry.Unix.cs | 41 +++++----- .../src/System/IO/FileStatus.Unix.cs | 76 +++++++++++++------ 2 files changed, 69 insertions(+), 48 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs index 78390ccdec28c2..8ea08cce3881f0 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs +++ b/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs @@ -37,7 +37,6 @@ internal static FileAttributes Initialize( // IMPORTANT: Attribute logic must match the logic in FileStatus bool isDirectory = false; - bool isSymlink = false; if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_DIR) { // We know it's a directory. @@ -46,41 +45,37 @@ internal static FileAttributes Initialize( // Some operating systems don't have the inode type in the dirent structure, // so we use DT_UNKNOWN as a sentinel value. As such, check if the dirent is a // directory. - else if ((directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK - || directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) - && Interop.Sys.Stat(entry.FullPath, out Interop.Sys.FileStatus targetStatus) >= 0) + else if ((directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK || + directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) && + Interop.Sys.Stat(entry.FullPath, out Interop.Sys.FileStatus statInfo) >= 0) { // Symlink or unknown: Stat to it to see if we can resolve it to a directory. - isDirectory = (targetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + isDirectory = FileStatus.IsDirectory(statInfo); } - // Same idea as the directory check, just repeated for (and tweaked due to the - // nature of) symlinks. + + // Same idea as the directory check, just repeated for (and tweaked due to the nature of) symlinks. + int resultLStat = Interop.Sys.LStat(entry.FullPath, out Interop.Sys.FileStatus lstatInfo); + + bool isReadOnly = resultLStat >= 0 && FileStatus.IsReadOnly(lstatInfo); + + bool isSymlink = false; if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK) { isSymlink = true; } - else if ((directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) - && (Interop.Sys.LStat(entry.FullPath, out Interop.Sys.FileStatus linkTargetStatus) >= 0)) + else if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN && resultLStat >= 0) { - isSymlink = (linkTargetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK; + isSymlink = FileStatus.IsSymLink(lstatInfo); } + bool isHidden = directoryEntry.Name[0] == '.' || (resultLStat >= 0 && FileStatus.IsHidden(lstatInfo)); + entry._status = default; FileStatus.Initialize(ref entry._status, isDirectory); - FileAttributes attributes = default; - if (isSymlink) - attributes |= FileAttributes.ReparsePoint; - if (isDirectory) - attributes |= FileAttributes.Directory; - if (directoryEntry.Name[0] == '.') - attributes |= FileAttributes.Hidden; - - if (attributes == default) - attributes = FileAttributes.Normal; - - entry._initialAttributes = attributes; - return attributes; + FileAttributes attributes = FileStatus.GetAttributes(isReadOnly, isSymlink, isDirectory, isHidden); + entry._initialAttributes = attributes; + return attributes; } private ReadOnlySpan FullPath diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs index e32d3a1c54dfdb..92d128d975b56c 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs +++ b/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs @@ -34,24 +34,32 @@ internal static void Initialize( status._fileStatusInitialized = -1; } - internal void Invalidate() => _fileStatusInitialized = -1; - internal bool IsReadOnly(ReadOnlySpan path, bool continueOnError = false) + internal static bool IsDirectory(Interop.Sys.FileStatus fileStatus) + { + return (fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + } + + internal static bool IsHidden(Interop.Sys.FileStatus fileStatus) + { + // If the filename starts with a period or has UF_HIDDEN flag set, it's hidden. + return (fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN; + } + + internal static bool IsReadOnly(Interop.Sys.FileStatus fileStatus) { - EnsureStatInitialized(path, continueOnError); #if TARGET_BROWSER const Interop.Sys.Permissions readBit = Interop.Sys.Permissions.S_IRUSR; const Interop.Sys.Permissions writeBit = Interop.Sys.Permissions.S_IWUSR; #else Interop.Sys.Permissions readBit, writeBit; - - if (_fileStatus.Uid == Interop.Sys.GetEUid()) + if (fileStatus.Uid == Interop.Sys.GetEUid()) { // User effectively owns the file readBit = Interop.Sys.Permissions.S_IRUSR; writeBit = Interop.Sys.Permissions.S_IWUSR; } - else if (_fileStatus.Gid == Interop.Sys.GetEGid()) + else if (fileStatus.Gid == Interop.Sys.GetEGid()) { // User belongs to a group that effectively owns the file readBit = Interop.Sys.Permissions.S_IRGRP; @@ -65,37 +73,55 @@ internal bool IsReadOnly(ReadOnlySpan path, bool continueOnError = false) } #endif - return ((_fileStatus.Mode & (int)readBit) != 0 && // has read permission - (_fileStatus.Mode & (int)writeBit) == 0); // but not write permission + return (fileStatus.Mode & (int)readBit) != 0 && // has read permission + (fileStatus.Mode & (int)writeBit) == 0; // but not write permission } - public FileAttributes GetAttributes(ReadOnlySpan path, ReadOnlySpan fileName) + internal static bool IsSymLink(Interop.Sys.FileStatus fileStatus) { - // IMPORTANT: Attribute logic must match the logic in FileSystemEntry - - EnsureStatInitialized(path); - - if (!_exists) - return (FileAttributes)(-1); + return (fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK; + } + internal static FileAttributes GetAttributes(bool isReadOnly, bool isSymlink, bool isDirectory, bool isHidden) + { FileAttributes attributes = default; - if (IsReadOnly(path)) + if (isReadOnly) attributes |= FileAttributes.ReadOnly; - - if ((_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK) + if (isSymlink) attributes |= FileAttributes.ReparsePoint; - - if (_isDirectory) + if (isDirectory) attributes |= FileAttributes.Directory; - - // If the filename starts with a period or has UF_HIDDEN flag set, it's hidden. - if (fileName.Length > 0 && (fileName[0] == '.' || (_fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN)) + if (isHidden) attributes |= FileAttributes.Hidden; return attributes != default ? attributes : FileAttributes.Normal; } + internal void Invalidate() => _fileStatusInitialized = -1; + + internal bool IsReadOnly(ReadOnlySpan path, bool continueOnError = false) + { + EnsureStatInitialized(path, continueOnError); + return IsReadOnly(_fileStatus); + } + + public FileAttributes GetAttributes(ReadOnlySpan path, ReadOnlySpan fileName) + { + // IMPORTANT: Attribute logic must match the logic in FileSystemEntry + + EnsureStatInitialized(path); + + if (!_exists) + return (FileAttributes)(-1); + + return GetAttributes( + IsReadOnly(path), + IsSymLink(_fileStatus), + _isDirectory, + (fileName.Length > 0 && fileName[0] == '.') || IsHidden(_fileStatus)); + } + public void SetAttributes(string path, FileAttributes attributes) { // Validate that only flags from the attribute are being provided. This is an @@ -300,13 +326,13 @@ public void Refresh(ReadOnlySpan path) _exists = true; // IMPORTANT: Is directory logic must match the logic in FileSystemEntry - _isDirectory = (_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + _isDirectory = IsDirectory(_fileStatus); // If we're a symlink, attempt to check the target to see if it is a directory if ((_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK && Interop.Sys.Stat(path, out Interop.Sys.FileStatus targetStatus) >= 0) { - _isDirectory = (targetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + _isDirectory = IsDirectory(targetStatus); } _fileStatusInitialized = 0; From cfb8175939dbd228c997b10fc41a5542c3788964 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 13 Aug 2020 15:42:22 -0700 Subject: [PATCH 2/4] Add missing check in public property FileSystemEntry.IsHidden. Address PR suggestions. --- .../IO/Enumeration/FileSystemEntry.Unix.cs | 18 ++++++----- .../src/System/IO/FileStatus.Unix.cs | 30 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs index 8ea08cce3881f0..8acbd9fccd06b4 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs +++ b/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs @@ -37,6 +37,7 @@ internal static FileAttributes Initialize( // IMPORTANT: Attribute logic must match the logic in FileStatus bool isDirectory = false; + bool isSymlink = false; if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_DIR) { // We know it's a directory. @@ -45,9 +46,9 @@ internal static FileAttributes Initialize( // Some operating systems don't have the inode type in the dirent structure, // so we use DT_UNKNOWN as a sentinel value. As such, check if the dirent is a // directory. - else if ((directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK || - directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) && - Interop.Sys.Stat(entry.FullPath, out Interop.Sys.FileStatus statInfo) >= 0) + else if ((directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK + || directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) + && Interop.Sys.Stat(entry.FullPath, out Interop.Sys.FileStatus statInfo) >= 0) { // Symlink or unknown: Stat to it to see if we can resolve it to a directory. isDirectory = FileStatus.IsDirectory(statInfo); @@ -58,24 +59,25 @@ internal static FileAttributes Initialize( bool isReadOnly = resultLStat >= 0 && FileStatus.IsReadOnly(lstatInfo); - bool isSymlink = false; if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK) { isSymlink = true; } - else if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN && resultLStat >= 0) + else if (resultLStat >= 0 && directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN) { isSymlink = FileStatus.IsSymLink(lstatInfo); } + // If the filename starts with a period or has UF_HIDDEN flag set, it's hidden. bool isHidden = directoryEntry.Name[0] == '.' || (resultLStat >= 0 && FileStatus.IsHidden(lstatInfo)); entry._status = default; FileStatus.Initialize(ref entry._status, isDirectory); FileAttributes attributes = FileStatus.GetAttributes(isReadOnly, isSymlink, isDirectory, isHidden); - entry._initialAttributes = attributes; - return attributes; + + entry._initialAttributes = attributes; + return attributes; } private ReadOnlySpan FullPath @@ -138,7 +140,7 @@ public FileAttributes Attributes public DateTimeOffset LastAccessTimeUtc => _status.GetLastAccessTime(FullPath, continueOnError: true); public DateTimeOffset LastWriteTimeUtc => _status.GetLastWriteTime(FullPath, continueOnError: true); public bool IsDirectory => _status.InitiallyDirectory; - public bool IsHidden => _directoryEntry.Name[0] == '.'; + public bool IsHidden => _directoryEntry.Name[0] == '.' || (Attributes & FileAttributes.Hidden) != 0; public FileSystemInfo ToFileSystemInfo() { diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs index 92d128d975b56c..3b638448ac3e6b 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs +++ b/src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs @@ -34,16 +34,12 @@ internal static void Initialize( status._fileStatusInitialized = -1; } + internal void Invalidate() => _fileStatusInitialized = -1; - internal static bool IsDirectory(Interop.Sys.FileStatus fileStatus) - { - return (fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; - } - - internal static bool IsHidden(Interop.Sys.FileStatus fileStatus) + internal bool IsReadOnly(ReadOnlySpan path, bool continueOnError = false) { - // If the filename starts with a period or has UF_HIDDEN flag set, it's hidden. - return (fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN; + EnsureStatInitialized(path, continueOnError); + return IsReadOnly(_fileStatus); } internal static bool IsReadOnly(Interop.Sys.FileStatus fileStatus) @@ -77,6 +73,16 @@ internal static bool IsReadOnly(Interop.Sys.FileStatus fileStatus) (fileStatus.Mode & (int)writeBit) == 0; // but not write permission } + internal static bool IsDirectory(Interop.Sys.FileStatus fileStatus) + { + return (fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + } + + internal static bool IsHidden(Interop.Sys.FileStatus fileStatus) + { + return (fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN; + } + internal static bool IsSymLink(Interop.Sys.FileStatus fileStatus) { return (fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK; @@ -98,14 +104,6 @@ internal static FileAttributes GetAttributes(bool isReadOnly, bool isSymlink, bo return attributes != default ? attributes : FileAttributes.Normal; } - internal void Invalidate() => _fileStatusInitialized = -1; - - internal bool IsReadOnly(ReadOnlySpan path, bool continueOnError = false) - { - EnsureStatInitialized(path, continueOnError); - return IsReadOnly(_fileStatus); - } - public FileAttributes GetAttributes(ReadOnlySpan path, ReadOnlySpan fileName) { // IMPORTANT: Attribute logic must match the logic in FileSystemEntry From ba00f54a56ae0cf8111a1d4a61b6e8f66b780dd1 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Thu, 13 Aug 2020 15:42:55 -0700 Subject: [PATCH 3/4] Add tests for Hidden and ReadOnly attribute check for each platform. Split existing Skip attribute test into different platforms. --- .../tests/Enumeration/AttributeTests.cs | 60 +++++++++++++++++-- .../tests/Enumeration/SkipAttributeTests.cs | 35 ++++++++--- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/Enumeration/AttributeTests.cs b/src/libraries/System.IO.FileSystem/tests/Enumeration/AttributeTests.cs index 3736c0669cf789..3974b95659077b 100644 --- a/src/libraries/System.IO.FileSystem/tests/Enumeration/AttributeTests.cs +++ b/src/libraries/System.IO.FileSystem/tests/Enumeration/AttributeTests.cs @@ -113,18 +113,42 @@ public void DirectoryAttributesAreExpected() } [Fact] - public void IsHiddenAttribute() + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)] + public void IsHiddenAttribute_Windows_OSX() { + // Put a period in front to make it hidden on Unix + IsHiddenAttributeInternal(useDotPrefix: false, useHiddenFlag: true); + + } + + + [Fact] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void IsHiddenAttribute_Unix() + { + // Windows and MacOS hide a file by setting the hidden attribute + IsHiddenAttributeInternal(useDotPrefix: true, useHiddenFlag: false); + } + + private void IsHiddenAttributeInternal(bool useDotPrefix, bool useHiddenFlag) + { + string prefix = useDotPrefix ? "." : ""; + DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); - FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); - // Put a period in front to make it hidden on Unix - FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, "." + GetTestFileName())); + FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); + FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, prefix + GetTestFileName())); fileOne.Create().Dispose(); fileTwo.Create().Dispose(); - if (PlatformDetection.IsWindows) - fileTwo.Attributes = fileTwo.Attributes | FileAttributes.Hidden; + + if (useHiddenFlag) + { + fileTwo.Attributes |= FileAttributes.Hidden; + } + + FileInfo fileCheck = new FileInfo(fileTwo.FullName); + Assert.Equal(fileTwo.Attributes, fileCheck.Attributes); IEnumerable enumerable = new FileSystemEnumerable( testDirectory.FullName, @@ -136,5 +160,29 @@ public void IsHiddenAttribute() Assert.Equal(new string[] { fileTwo.FullName }, enumerable); } + + [Fact] + public void IsReadOnlyAttribute() + { + DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); + + FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); + FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); + + fileOne.Create().Dispose(); + fileTwo.Create().Dispose(); + + fileTwo.Attributes |= FileAttributes.ReadOnly; + + IEnumerable enumerable = new FileSystemEnumerable( + testDirectory.FullName, + (ref FileSystemEntry entry) => entry.ToFullPath(), + new EnumerationOptions() { AttributesToSkip = 0 }) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => (entry.Attributes & FileAttributes.ReadOnly) != 0 + }; + + Assert.Equal(new string[] { fileTwo.FullName }, enumerable); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.cs b/src/libraries/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.cs index 3a22c61b6f77e6..cb79200bc17809 100644 --- a/src/libraries/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.cs +++ b/src/libraries/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.cs @@ -21,25 +21,42 @@ protected virtual string[] GetPaths(string directory, EnumerationOptions options } [Fact] - public void SkippingHiddenFiles() + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.OSX)] + public void SkippingHiddenFiles_Windows_OSX() + { + SkippingHiddenFilesInternal(useDotPrefix: false, useHiddenFlag: true); + } + + [Fact] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void SkippingHiddenFiles_Unix() + { + SkippingHiddenFilesInternal(useDotPrefix: true, useHiddenFlag: false); + } + + private void SkippingHiddenFilesInternal(bool useDotPrefix, bool useHiddenFlag) { DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); DirectoryInfo testSubdirectory = Directory.CreateDirectory(Path.Combine(testDirectory.FullName, GetTestFileName())); - FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); - // Put a period in front to make it hidden on Unix - FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, "." + GetTestFileName())); + FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); FileInfo fileThree = new FileInfo(Path.Combine(testSubdirectory.FullName, GetTestFileName())); - FileInfo fileFour = new FileInfo(Path.Combine(testSubdirectory.FullName, "." + GetTestFileName())); + + // Put a period in front of files two and four to make them hidden on Unix + string prefix = useDotPrefix ? "." : ""; + FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, prefix + GetTestFileName())); + FileInfo fileFour = new FileInfo(Path.Combine(testSubdirectory.FullName, prefix + GetTestFileName())); fileOne.Create().Dispose(); fileTwo.Create().Dispose(); - if (PlatformDetection.IsWindows) - fileTwo.Attributes = fileTwo.Attributes | FileAttributes.Hidden; fileThree.Create().Dispose(); fileFour.Create().Dispose(); - if (PlatformDetection.IsWindows) - fileFour.Attributes = fileTwo.Attributes | FileAttributes.Hidden; + + if (useHiddenFlag) + { + fileTwo.Attributes |= FileAttributes.Hidden; + fileFour.Attributes |= FileAttributes.Hidden; + } // Default EnumerationOptions is to skip hidden string[] paths = GetPaths(testDirectory.FullName, new EnumerationOptions()); From a39bf8a0e548885d0dc7830e4bfc6ff7932b5fd2 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 14 Aug 2020 11:00:52 -0700 Subject: [PATCH 4/4] Use _initialAttributes instead of Attributes for IsHidden. Add comment on top. --- .../src/System/IO/Enumeration/FileSystemEntry.Unix.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs index 8acbd9fccd06b4..2880aaa5df8e1d 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs +++ b/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs @@ -140,7 +140,12 @@ public FileAttributes Attributes public DateTimeOffset LastAccessTimeUtc => _status.GetLastAccessTime(FullPath, continueOnError: true); public DateTimeOffset LastWriteTimeUtc => _status.GetLastWriteTime(FullPath, continueOnError: true); public bool IsDirectory => _status.InitiallyDirectory; - public bool IsHidden => _directoryEntry.Name[0] == '.' || (Attributes & FileAttributes.Hidden) != 0; + /// + /// Returns if the file is hidden; otherwise. + /// In Linux and OSX, a file can be marked hidden if the filename is prepended with a dot. + /// In Windows and OSX, a file can be hidden if the special hidden attribute is set. For example, via the enum flag. + /// + public bool IsHidden => _directoryEntry.Name[0] == '.' || (_initialAttributes & FileAttributes.Hidden) != 0; public FileSystemInfo ToFileSystemInfo() {