Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 597f44c

Browse files
authored
Handle errors getting state in Unix (#27239)
* Handle errors getting state in Unix Throwing errors while examining extended state while enumerating isn't consistent with Windows behavior. Windows never throws past getting directory entry data as all state is already available. Ensure entry attribute state is consistent with initial construction. * Win 7 CI machines are also setting NotContentIndexed.
1 parent 6993b6d commit 597f44c

File tree

5 files changed

+178
-45
lines changed

5 files changed

+178
-45
lines changed

src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public unsafe ref struct FileSystemEntry
1616
private ReadOnlySpan<char> _fullPath;
1717
private ReadOnlySpan<char> _fileName;
1818
private fixed char _fileNameBuffer[FileNameBufferSize];
19+
private FileAttributes _initialAttributes;
1920

2021
internal static FileAttributes Initialize(
2122
ref FileSystemEntry entry,
@@ -51,14 +52,18 @@ internal static FileAttributes Initialize(
5152
entry._status = default;
5253
FileStatus.Initialize(ref entry._status, isDirectory);
5354

54-
FileAttributes attributes = FileAttributes.Normal;
55+
FileAttributes attributes = default;
5556
if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_LNK)
5657
attributes |= FileAttributes.ReparsePoint;
5758
if (isDirectory)
5859
attributes |= FileAttributes.Directory;
5960
if (directoryEntry.Name[0] == '.')
6061
attributes |= FileAttributes.Hidden;
6162

63+
if (attributes == default)
64+
attributes = FileAttributes.Normal;
65+
66+
entry._initialAttributes = attributes;
6267
return attributes;
6368
}
6469

@@ -112,11 +117,17 @@ public ReadOnlySpan<char> FileName
112117
/// </summary>
113118
public ReadOnlySpan<char> OriginalRootDirectory { get; private set; }
114119

115-
public FileAttributes Attributes => _status.GetAttributes(FullPath, FileName);
116-
public long Length => _status.GetLength(FullPath);
117-
public DateTimeOffset CreationTimeUtc => _status.GetCreationTime(FullPath);
118-
public DateTimeOffset LastAccessTimeUtc => _status.GetLastAccessTime(FullPath);
119-
public DateTimeOffset LastWriteTimeUtc => _status.GetLastWriteTime(FullPath);
120+
// Windows never fails getting attributes, length, or time as that information comes back
121+
// with the native enumeration struct. As such we must not throw here.
122+
123+
public FileAttributes Attributes
124+
// It would be hard to rationalize if the attributes change after our initial find.
125+
=> _initialAttributes | (_status.IsReadOnly(FullPath, continueOnError: true) ? FileAttributes.ReadOnly : 0);
126+
127+
public long Length => _status.GetLength(FullPath, continueOnError: true);
128+
public DateTimeOffset CreationTimeUtc => _status.GetCreationTime(FullPath, continueOnError: true);
129+
public DateTimeOffset LastAccessTimeUtc => _status.GetLastAccessTime(FullPath, continueOnError: true);
130+
public DateTimeOffset LastWriteTimeUtc => _status.GetLastWriteTime(FullPath, continueOnError: true);
120131
public bool IsDirectory => _status.InitiallyDirectory;
121132

122133
public FileSystemInfo ToFileSystemInfo()

src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Unix.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,20 @@ public FileSystemEnumerator(string directory, EnumerationOptions options = null)
6666
}
6767
}
6868

69+
private bool InternalContinueOnError(int error)
70+
=> (_options.IgnoreInaccessible && IsAccessError(error)) || ContinueOnError(error);
71+
72+
private static bool IsAccessError(int error)
73+
=> error == (int)Interop.Error.EACCES || error == (int)Interop.Error.EBADF
74+
|| error == (int)Interop.Error.EPERM;
75+
6976
private IntPtr CreateDirectoryHandle(string path)
7077
{
7178
IntPtr handle = Interop.Sys.OpenDir(path);
7279
if (handle == IntPtr.Zero)
7380
{
7481
Interop.ErrorInfo info = Interop.Sys.GetLastErrorInfo();
75-
if ((_options.IgnoreInaccessible && IsAccessError(info.RawErrno))
76-
|| ContinueOnError(info.RawErrno))
82+
if (InternalContinueOnError(info.RawErrno))
7783
{
7884
return IntPtr.Zero;
7985
}
@@ -107,7 +113,8 @@ public bool MoveNext()
107113
if (_lastEntryFound)
108114
return false;
109115

110-
FileAttributes attributes = FileSystemEntry.Initialize(ref entry, _entry, _currentPath, _rootDirectory, _originalRootDirectory, new Span<char>(_pathBuffer));
116+
FileAttributes attributes = FileSystemEntry.Initialize(
117+
ref entry, _entry, _currentPath, _rootDirectory, _originalRootDirectory, new Span<char>(_pathBuffer));
111118
bool isDirectory = (attributes & FileAttributes.Directory) != 0;
112119

113120
bool isSpecialDirectory = false;
@@ -131,7 +138,7 @@ public bool MoveNext()
131138
attributes = entry.Attributes;
132139
}
133140

134-
if (attributes != (FileAttributes)(-1) && (_options.AttributesToSkip & attributes) != 0)
141+
if ((_options.AttributesToSkip & attributes) != 0)
135142
{
136143
continue;
137144
}
@@ -172,8 +179,7 @@ private unsafe void FindNextEntry()
172179
break;
173180
default:
174181
// Error
175-
if ((_options.IgnoreInaccessible && IsAccessError(result))
176-
|| ContinueOnError(result))
182+
if (InternalContinueOnError(result))
177183
{
178184
DirectoryFinished();
179185
break;
@@ -191,10 +197,6 @@ private void DequeueNextDirectory()
191197
_directoryHandle = CreateDirectoryHandle(_currentPath);
192198
}
193199

194-
private static bool IsAccessError(int error)
195-
=> error == (int)Interop.Error.EACCES || error == (int)Interop.Error.EBADF
196-
|| error == (int)Interop.Error.EPERM;
197-
198200
private void InternalDispose(bool disposing)
199201
{
200202
// It is possible to fail to allocate the lock, but the finalizer will still run

src/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,9 @@ internal static void Initialize(
3535

3636
internal void Invalidate() => _fileStatusInitialized = -1;
3737

38-
public FileAttributes GetAttributes(ReadOnlySpan<char> path, ReadOnlySpan<char> fileName)
38+
internal bool IsReadOnly(ReadOnlySpan<char> path, bool continueOnError = false)
3939
{
40-
// IMPORTANT: Attribute logic must match the logic in FileSystemEntry
41-
42-
EnsureStatInitialized(path);
43-
44-
if (!_exists)
45-
return (FileAttributes)(-1);
46-
47-
FileAttributes attrs = default;
48-
40+
EnsureStatInitialized(path, continueOnError);
4941
Interop.Sys.Permissions readBit, writeBit;
5042
if (_fileStatus.Uid == Interop.Sys.GetEUid())
5143
{
@@ -66,23 +58,35 @@ public FileAttributes GetAttributes(ReadOnlySpan<char> path, ReadOnlySpan<char>
6658
writeBit = Interop.Sys.Permissions.S_IWOTH;
6759
}
6860

69-
if ((_fileStatus.Mode & (int)readBit) != 0 && // has read permission
70-
(_fileStatus.Mode & (int)writeBit) == 0) // but not write permission
71-
{
72-
attrs |= FileAttributes.ReadOnly;
73-
}
61+
return ((_fileStatus.Mode & (int)readBit) != 0 && // has read permission
62+
(_fileStatus.Mode & (int)writeBit) == 0); // but not write permission
63+
}
64+
65+
public FileAttributes GetAttributes(ReadOnlySpan<char> path, ReadOnlySpan<char> fileName)
66+
{
67+
// IMPORTANT: Attribute logic must match the logic in FileSystemEntry
68+
69+
EnsureStatInitialized(path);
70+
71+
if (!_exists)
72+
return (FileAttributes)(-1);
73+
74+
FileAttributes attributes = default;
75+
76+
if (IsReadOnly(path))
77+
attributes |= FileAttributes.ReadOnly;
7478

7579
if ((_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK)
76-
attrs |= FileAttributes.ReparsePoint;
80+
attributes |= FileAttributes.ReparsePoint;
7781

7882
if (_isDirectory)
79-
attrs |= FileAttributes.Directory;
83+
attributes |= FileAttributes.Directory;
8084

8185
// If the filename starts with a period, it's hidden.
8286
if (fileName.Length > 0 && fileName[0] == '.')
83-
attrs |= FileAttributes.Hidden;
87+
attributes |= FileAttributes.Hidden;
8488

85-
return attrs != default ? attrs : FileAttributes.Normal;
89+
return attributes != default ? attributes : FileAttributes.Normal;
8690
}
8791

8892
public void SetAttributes(string path, FileAttributes attributes)
@@ -138,9 +142,9 @@ internal bool GetExists(ReadOnlySpan<char> path)
138142
return _exists && InitiallyDirectory == _isDirectory;
139143
}
140144

141-
internal DateTimeOffset GetCreationTime(ReadOnlySpan<char> path)
145+
internal DateTimeOffset GetCreationTime(ReadOnlySpan<char> path, bool continueOnError = false)
142146
{
143-
EnsureStatInitialized(path);
147+
EnsureStatInitialized(path, continueOnError);
144148
if (!_exists)
145149
return DateTimeOffset.FromFileTime(0);
146150

@@ -163,9 +167,9 @@ internal void SetCreationTime(string path, DateTimeOffset time)
163167
SetLastAccessTime(path, time);
164168
}
165169

166-
internal DateTimeOffset GetLastAccessTime(ReadOnlySpan<char> path)
170+
internal DateTimeOffset GetLastAccessTime(ReadOnlySpan<char> path, bool continueOnError = false)
167171
{
168-
EnsureStatInitialized(path);
172+
EnsureStatInitialized(path, continueOnError);
169173
if (!_exists)
170174
return DateTimeOffset.FromFileTime(0);
171175
return UnixTimeToDateTimeOffset(_fileStatus.ATime, _fileStatus.ATimeNsec);
@@ -174,9 +178,9 @@ internal DateTimeOffset GetLastAccessTime(ReadOnlySpan<char> path)
174178
internal void SetLastAccessTime(string path, DateTimeOffset time)
175179
=> SetAccessWriteTimes(path, time.ToUnixTimeSeconds(), null);
176180

177-
internal DateTimeOffset GetLastWriteTime(ReadOnlySpan<char> path)
181+
internal DateTimeOffset GetLastWriteTime(ReadOnlySpan<char> path, bool continueOnError = false)
178182
{
179-
EnsureStatInitialized(path);
183+
EnsureStatInitialized(path, continueOnError);
180184
if (!_exists)
181185
return DateTimeOffset.FromFileTime(0);
182186
return UnixTimeToDateTimeOffset(_fileStatus.MTime, _fileStatus.MTimeNsec);
@@ -203,9 +207,9 @@ private void SetAccessWriteTimes(string path, long? accessTime, long? writeTime)
203207
_fileStatusInitialized = -1;
204208
}
205209

206-
internal long GetLength(ReadOnlySpan<char> path)
210+
internal long GetLength(ReadOnlySpan<char> path, bool continueOnError = false)
207211
{
208-
EnsureStatInitialized(path);
212+
EnsureStatInitialized(path, continueOnError);
209213
return _fileStatus.Size;
210214
}
211215

@@ -256,14 +260,14 @@ public void Refresh(ReadOnlySpan<char> path)
256260
_fileStatusInitialized = 0;
257261
}
258262

259-
internal void EnsureStatInitialized(ReadOnlySpan<char> path)
263+
internal void EnsureStatInitialized(ReadOnlySpan<char> path, bool continueOnError = false)
260264
{
261265
if (_fileStatusInitialized == -1)
262266
{
263267
Refresh(path);
264268
}
265269

266-
if (_fileStatusInitialized != 0)
270+
if (_fileStatusInitialized != 0 && !continueOnError)
267271
{
268272
int errno = _fileStatusInitialized;
269273
_fileStatusInitialized = -1;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.IO.Enumeration;
6+
using Xunit;
7+
8+
namespace System.IO.Tests.Enumeration
9+
{
10+
public class AttributeTests : FileSystemTest
11+
{
12+
private class DefaultFileAttributes : FileSystemEnumerator<string>
13+
{
14+
public DefaultFileAttributes(string directory, EnumerationOptions options)
15+
: base(directory, options)
16+
{
17+
}
18+
19+
protected override bool ContinueOnError(int error)
20+
{
21+
Assert.False(true, $"Should not have errored {error}");
22+
return false;
23+
}
24+
25+
protected override bool ShouldIncludeEntry(ref FileSystemEntry entry)
26+
=> !entry.IsDirectory;
27+
28+
protected override string TransformEntry(ref FileSystemEntry entry)
29+
{
30+
string path = entry.ToFullPath();
31+
File.Delete(path);
32+
33+
// Attributes require a stat call on Unix- ensure that we have the right attributes
34+
// even if the returned file is deleted.
35+
Assert.Equal(FileAttributes.Normal, entry.Attributes);
36+
Assert.Equal(path, entry.ToFullPath());
37+
return new string(entry.FileName);
38+
}
39+
}
40+
41+
[Fact]
42+
public void FileAttributesAreExpected()
43+
{
44+
DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath());
45+
FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName()));
46+
47+
fileOne.Create().Dispose();
48+
49+
if (PlatformDetection.IsWindows)
50+
{
51+
// Archive should always be set on a new file. Clear it and other expected flags to
52+
// see that we get "Normal" as the default when enumerating.
53+
54+
Assert.True((fileOne.Attributes & FileAttributes.Archive) != 0);
55+
fileOne.Attributes &= ~(FileAttributes.Archive | FileAttributes.NotContentIndexed);
56+
}
57+
58+
using (var enumerator = new DefaultFileAttributes(testDirectory.FullName, new EnumerationOptions()))
59+
{
60+
Assert.True(enumerator.MoveNext());
61+
Assert.Equal(fileOne.Name, enumerator.Current);
62+
Assert.False(enumerator.MoveNext());
63+
}
64+
}
65+
66+
private class DefaultDirectoryAttributes : FileSystemEnumerator<string>
67+
{
68+
public DefaultDirectoryAttributes(string directory, EnumerationOptions options)
69+
: base(directory, options)
70+
{
71+
}
72+
73+
protected override bool ShouldIncludeEntry(ref FileSystemEntry entry)
74+
=> entry.IsDirectory;
75+
76+
protected override bool ContinueOnError(int error)
77+
{
78+
Assert.False(true, $"Should not have errored {error}");
79+
return false;
80+
}
81+
82+
protected override string TransformEntry(ref FileSystemEntry entry)
83+
{
84+
string path = entry.ToFullPath();
85+
Directory.Delete(path);
86+
87+
// Attributes require a stat call on Unix- ensure that we have the right attributes
88+
// even if the returned directory is deleted.
89+
Assert.Equal(FileAttributes.Directory, entry.Attributes);
90+
Assert.Equal(path, entry.ToFullPath());
91+
return new string(entry.FileName);
92+
}
93+
}
94+
95+
[Fact]
96+
public void DirectoryAttributesAreExpected()
97+
{
98+
DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath());
99+
DirectoryInfo subDirectory = Directory.CreateDirectory(Path.Combine(testDirectory.FullName, GetTestFileName()));
100+
101+
if (PlatformDetection.IsWindows)
102+
{
103+
// Clear possible extra flags to see that we get Directory
104+
subDirectory.Attributes &= ~FileAttributes.NotContentIndexed;
105+
}
106+
107+
using (var enumerator = new DefaultDirectoryAttributes(testDirectory.FullName, new EnumerationOptions()))
108+
{
109+
Assert.True(enumerator.MoveNext());
110+
Assert.Equal(subDirectory.Name, enumerator.Current);
111+
Assert.False(enumerator.MoveNext());
112+
}
113+
}
114+
}
115+
}

src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
<Compile Include="Enumeration\IncludePredicateTests.netcoreapp.cs" />
6464
<Compile Include="Enumeration\PatternTransformTests.netcoreapp.cs" />
6565
<Compile Include="Enumeration\RootTests.netcoreapp.cs" />
66+
<Compile Include="Enumeration\AttributeTests.netcoreapp.cs" />
6667
</ItemGroup>
6768
<ItemGroup>
6869
<!-- Rewritten -->

0 commit comments

Comments
 (0)