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..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
@@ -48,36 +48,33 @@ internal static FileAttributes Initialize(
// 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)
+ && 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);
+
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 (resultLStat >= 0 && directoryEntry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN)
{
- isSymlink = (linkTargetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK;
+ 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 = default;
- if (isSymlink)
- attributes |= FileAttributes.ReparsePoint;
- if (isDirectory)
- attributes |= FileAttributes.Directory;
- if (directoryEntry.Name[0] == '.')
- attributes |= FileAttributes.Hidden;
-
- if (attributes == default)
- attributes = FileAttributes.Normal;
+ FileAttributes attributes = FileStatus.GetAttributes(isReadOnly, isSymlink, isDirectory, isHidden);
entry._initialAttributes = attributes;
return attributes;
@@ -143,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] == '.';
+ ///
+ /// 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()
{
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..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
@@ -39,19 +39,23 @@ internal static void Initialize(
internal bool IsReadOnly(ReadOnlySpan path, bool continueOnError = false)
{
EnsureStatInitialized(path, continueOnError);
+ return IsReadOnly(_fileStatus);
+ }
+
+ internal static bool IsReadOnly(Interop.Sys.FileStatus fileStatus)
+ {
#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 +69,57 @@ 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 IsDirectory(Interop.Sys.FileStatus fileStatus)
{
- // IMPORTANT: Attribute logic must match the logic in FileSystemEntry
+ return (fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR;
+ }
- EnsureStatInitialized(path);
+ internal static bool IsHidden(Interop.Sys.FileStatus fileStatus)
+ {
+ return (fileStatus.UserFlags & (uint)Interop.Sys.UserFlags.UF_HIDDEN) == (uint)Interop.Sys.UserFlags.UF_HIDDEN;
+ }
- if (!_exists)
- return (FileAttributes)(-1);
+ internal static bool IsSymLink(Interop.Sys.FileStatus fileStatus)
+ {
+ 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;
}
+ 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 +324,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;
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());