diff options
5 files changed, 178 insertions, 45 deletions
diff --git a/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs index ccf6f29744..f5339377b3 100644 --- a/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs @@ -16,6 +16,7 @@ namespace System.IO.Enumeration private ReadOnlySpan<char> _fullPath; private ReadOnlySpan<char> _fileName; private fixed char _fileNameBuffer[FileNameBufferSize]; + private FileAttributes _initialAttributes; internal static FileAttributes Initialize( ref FileSystemEntry entry, @@ -51,7 +52,7 @@ namespace System.IO.Enumeration entry._status = default; FileStatus.Initialize(ref entry._status, isDirectory); - FileAttributes attributes = FileAttributes.Normal; + FileAttributes attributes = default; if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK) attributes |= FileAttributes.ReparsePoint; if (isDirectory) @@ -59,6 +60,10 @@ namespace System.IO.Enumeration if (directoryEntry.Name[0] == '.') attributes |= FileAttributes.Hidden; + if (attributes == default) + attributes = FileAttributes.Normal; + + entry._initialAttributes = attributes; return attributes; } @@ -112,11 +117,17 @@ namespace System.IO.Enumeration /// </summary> public ReadOnlySpan<char> OriginalRootDirectory { get; private set; } - public FileAttributes Attributes => _status.GetAttributes(FullPath, FileName); - public long Length => _status.GetLength(FullPath); - public DateTimeOffset CreationTimeUtc => _status.GetCreationTime(FullPath); - public DateTimeOffset LastAccessTimeUtc => _status.GetLastAccessTime(FullPath); - public DateTimeOffset LastWriteTimeUtc => _status.GetLastWriteTime(FullPath); + // Windows never fails getting attributes, length, or time as that information comes back + // with the native enumeration struct. As such we must not throw here. + + public FileAttributes Attributes + // It would be hard to rationalize if the attributes change after our initial find. + => _initialAttributes | (_status.IsReadOnly(FullPath, continueOnError: true) ? FileAttributes.ReadOnly : 0); + + public long Length => _status.GetLength(FullPath, continueOnError: true); + public DateTimeOffset CreationTimeUtc => _status.GetCreationTime(FullPath, continueOnError: true); + public DateTimeOffset LastAccessTimeUtc => _status.GetLastAccessTime(FullPath, continueOnError: true); + public DateTimeOffset LastWriteTimeUtc => _status.GetLastWriteTime(FullPath, continueOnError: true); public bool IsDirectory => _status.InitiallyDirectory; public FileSystemInfo ToFileSystemInfo() diff --git a/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Unix.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Unix.cs index 4ec505ff66..cfc3438260 100644 --- a/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Unix.cs +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Unix.cs @@ -66,14 +66,20 @@ namespace System.IO.Enumeration } } + private bool InternalContinueOnError(int error) + => (_options.IgnoreInaccessible && IsAccessError(error)) || ContinueOnError(error); + + private static bool IsAccessError(int error) + => error == (int)Interop.Error.EACCES || error == (int)Interop.Error.EBADF + || error == (int)Interop.Error.EPERM; + private IntPtr CreateDirectoryHandle(string path) { IntPtr handle = Interop.Sys.OpenDir(path); if (handle == IntPtr.Zero) { Interop.ErrorInfo info = Interop.Sys.GetLastErrorInfo(); - if ((_options.IgnoreInaccessible && IsAccessError(info.RawErrno)) - || ContinueOnError(info.RawErrno)) + if (InternalContinueOnError(info.RawErrno)) { return IntPtr.Zero; } @@ -107,7 +113,8 @@ namespace System.IO.Enumeration if (_lastEntryFound) return false; - FileAttributes attributes = FileSystemEntry.Initialize(ref entry, _entry, _currentPath, _rootDirectory, _originalRootDirectory, new Span<char>(_pathBuffer)); + FileAttributes attributes = FileSystemEntry.Initialize( + ref entry, _entry, _currentPath, _rootDirectory, _originalRootDirectory, new Span<char>(_pathBuffer)); bool isDirectory = (attributes & FileAttributes.Directory) != 0; bool isSpecialDirectory = false; @@ -131,7 +138,7 @@ namespace System.IO.Enumeration attributes = entry.Attributes; } - if (attributes != (FileAttributes)(-1) && (_options.AttributesToSkip & attributes) != 0) + if ((_options.AttributesToSkip & attributes) != 0) { continue; } @@ -172,8 +179,7 @@ namespace System.IO.Enumeration break; default: // Error - if ((_options.IgnoreInaccessible && IsAccessError(result)) - || ContinueOnError(result)) + if (InternalContinueOnError(result)) { DirectoryFinished(); break; @@ -191,10 +197,6 @@ namespace System.IO.Enumeration _directoryHandle = CreateDirectoryHandle(_currentPath); } - private static bool IsAccessError(int error) - => error == (int)Interop.Error.EACCES || error == (int)Interop.Error.EBADF - || error == (int)Interop.Error.EPERM; - private void InternalDispose(bool disposing) { // It is possible to fail to allocate the lock, but the finalizer will still run diff --git a/src/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs b/src/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs index 387cc32bfd..07a48d54e1 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs @@ -35,17 +35,9 @@ namespace System.IO internal void Invalidate() => _fileStatusInitialized = -1; - public FileAttributes GetAttributes(ReadOnlySpan<char> path, ReadOnlySpan<char> fileName) + internal bool IsReadOnly(ReadOnlySpan<char> path, bool continueOnError = false) { - // IMPORTANT: Attribute logic must match the logic in FileSystemEntry - - EnsureStatInitialized(path); - - if (!_exists) - return (FileAttributes)(-1); - - FileAttributes attrs = default; - + EnsureStatInitialized(path, continueOnError); Interop.Sys.Permissions readBit, writeBit; if (_fileStatus.Uid == Interop.Sys.GetEUid()) { @@ -66,23 +58,35 @@ namespace System.IO writeBit = Interop.Sys.Permissions.S_IWOTH; } - if ((_fileStatus.Mode & (int)readBit) != 0 && // has read permission - (_fileStatus.Mode & (int)writeBit) == 0) // but not write permission - { - attrs |= FileAttributes.ReadOnly; - } + return ((_fileStatus.Mode & (int)readBit) != 0 && // has read permission + (_fileStatus.Mode & (int)writeBit) == 0); // but not write permission + } + + public FileAttributes GetAttributes(ReadOnlySpan<char> path, ReadOnlySpan<char> fileName) + { + // IMPORTANT: Attribute logic must match the logic in FileSystemEntry + + EnsureStatInitialized(path); + + if (!_exists) + return (FileAttributes)(-1); + + FileAttributes attributes = default; + + if (IsReadOnly(path)) + attributes |= FileAttributes.ReadOnly; if ((_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK) - attrs |= FileAttributes.ReparsePoint; + attributes |= FileAttributes.ReparsePoint; if (_isDirectory) - attrs |= FileAttributes.Directory; + attributes |= FileAttributes.Directory; // If the filename starts with a period, it's hidden. if (fileName.Length > 0 && fileName[0] == '.') - attrs |= FileAttributes.Hidden; + attributes |= FileAttributes.Hidden; - return attrs != default ? attrs : FileAttributes.Normal; + return attributes != default ? attributes : FileAttributes.Normal; } public void SetAttributes(string path, FileAttributes attributes) @@ -138,9 +142,9 @@ namespace System.IO return _exists && InitiallyDirectory == _isDirectory; } - internal DateTimeOffset GetCreationTime(ReadOnlySpan<char> path) + internal DateTimeOffset GetCreationTime(ReadOnlySpan<char> path, bool continueOnError = false) { - EnsureStatInitialized(path); + EnsureStatInitialized(path, continueOnError); if (!_exists) return DateTimeOffset.FromFileTime(0); @@ -163,9 +167,9 @@ namespace System.IO SetLastAccessTime(path, time); } - internal DateTimeOffset GetLastAccessTime(ReadOnlySpan<char> path) + internal DateTimeOffset GetLastAccessTime(ReadOnlySpan<char> path, bool continueOnError = false) { - EnsureStatInitialized(path); + EnsureStatInitialized(path, continueOnError); if (!_exists) return DateTimeOffset.FromFileTime(0); return UnixTimeToDateTimeOffset(_fileStatus.ATime, _fileStatus.ATimeNsec); @@ -174,9 +178,9 @@ namespace System.IO internal void SetLastAccessTime(string path, DateTimeOffset time) => SetAccessWriteTimes(path, time.ToUnixTimeSeconds(), null); - internal DateTimeOffset GetLastWriteTime(ReadOnlySpan<char> path) + internal DateTimeOffset GetLastWriteTime(ReadOnlySpan<char> path, bool continueOnError = false) { - EnsureStatInitialized(path); + EnsureStatInitialized(path, continueOnError); if (!_exists) return DateTimeOffset.FromFileTime(0); return UnixTimeToDateTimeOffset(_fileStatus.MTime, _fileStatus.MTimeNsec); @@ -203,9 +207,9 @@ namespace System.IO _fileStatusInitialized = -1; } - internal long GetLength(ReadOnlySpan<char> path) + internal long GetLength(ReadOnlySpan<char> path, bool continueOnError = false) { - EnsureStatInitialized(path); + EnsureStatInitialized(path, continueOnError); return _fileStatus.Size; } @@ -256,14 +260,14 @@ namespace System.IO _fileStatusInitialized = 0; } - internal void EnsureStatInitialized(ReadOnlySpan<char> path) + internal void EnsureStatInitialized(ReadOnlySpan<char> path, bool continueOnError = false) { if (_fileStatusInitialized == -1) { Refresh(path); } - if (_fileStatusInitialized != 0) + if (_fileStatusInitialized != 0 && !continueOnError) { int errno = _fileStatusInitialized; _fileStatusInitialized = -1; diff --git a/src/System.IO.FileSystem/tests/Enumeration/AttributeTests.netcoreapp.cs b/src/System.IO.FileSystem/tests/Enumeration/AttributeTests.netcoreapp.cs new file mode 100644 index 0000000000..d35783713f --- /dev/null +++ b/src/System.IO.FileSystem/tests/Enumeration/AttributeTests.netcoreapp.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO.Enumeration; +using Xunit; + +namespace System.IO.Tests.Enumeration +{ + public class AttributeTests : FileSystemTest + { + private class DefaultFileAttributes : FileSystemEnumerator<string> + { + public DefaultFileAttributes(string directory, EnumerationOptions options) + : base(directory, options) + { + } + + protected override bool ContinueOnError(int error) + { + Assert.False(true, $"Should not have errored {error}"); + return false; + } + + protected override bool ShouldIncludeEntry(ref FileSystemEntry entry) + => !entry.IsDirectory; + + protected override string TransformEntry(ref FileSystemEntry entry) + { + string path = entry.ToFullPath(); + File.Delete(path); + + // Attributes require a stat call on Unix- ensure that we have the right attributes + // even if the returned file is deleted. + Assert.Equal(FileAttributes.Normal, entry.Attributes); + Assert.Equal(path, entry.ToFullPath()); + return new string(entry.FileName); + } + } + + [Fact] + public void FileAttributesAreExpected() + { + DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); + FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); + + fileOne.Create().Dispose(); + + if (PlatformDetection.IsWindows) + { + // Archive should always be set on a new file. Clear it and other expected flags to + // see that we get "Normal" as the default when enumerating. + + Assert.True((fileOne.Attributes & FileAttributes.Archive) != 0); + fileOne.Attributes &= ~(FileAttributes.Archive | FileAttributes.NotContentIndexed); + } + + using (var enumerator = new DefaultFileAttributes(testDirectory.FullName, new EnumerationOptions())) + { + Assert.True(enumerator.MoveNext()); + Assert.Equal(fileOne.Name, enumerator.Current); + Assert.False(enumerator.MoveNext()); + } + } + + private class DefaultDirectoryAttributes : FileSystemEnumerator<string> + { + public DefaultDirectoryAttributes(string directory, EnumerationOptions options) + : base(directory, options) + { + } + + protected override bool ShouldIncludeEntry(ref FileSystemEntry entry) + => entry.IsDirectory; + + protected override bool ContinueOnError(int error) + { + Assert.False(true, $"Should not have errored {error}"); + return false; + } + + protected override string TransformEntry(ref FileSystemEntry entry) + { + string path = entry.ToFullPath(); + Directory.Delete(path); + + // Attributes require a stat call on Unix- ensure that we have the right attributes + // even if the returned directory is deleted. + Assert.Equal(FileAttributes.Directory, entry.Attributes); + Assert.Equal(path, entry.ToFullPath()); + return new string(entry.FileName); + } + } + + [Fact] + public void DirectoryAttributesAreExpected() + { + DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); + DirectoryInfo subDirectory = Directory.CreateDirectory(Path.Combine(testDirectory.FullName, GetTestFileName())); + + if (PlatformDetection.IsWindows) + { + // Clear possible extra flags to see that we get Directory + subDirectory.Attributes &= ~FileAttributes.NotContentIndexed; + } + + using (var enumerator = new DefaultDirectoryAttributes(testDirectory.FullName, new EnumerationOptions())) + { + Assert.True(enumerator.MoveNext()); + Assert.Equal(subDirectory.Name, enumerator.Current); + Assert.False(enumerator.MoveNext()); + } + } + } +} diff --git a/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index 402489b5e8..672d5642dc 100644 --- a/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -63,6 +63,7 @@ <Compile Include="Enumeration\IncludePredicateTests.netcoreapp.cs" /> <Compile Include="Enumeration\PatternTransformTests.netcoreapp.cs" /> <Compile Include="Enumeration\RootTests.netcoreapp.cs" /> + <Compile Include="Enumeration\AttributeTests.netcoreapp.cs" /> </ItemGroup> <ItemGroup> <!-- Rewritten --> |