-
Notifications
You must be signed in to change notification settings - Fork 4.6k
/
FileSystemEnumerator.Unix.cs
260 lines (222 loc) · 10.4 KB
/
FileSystemEnumerator.Unix.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.Collections.Generic;
using System.Runtime.ConstrainedExecution;
using System.Threading;
namespace System.IO.Enumeration
{
public abstract unsafe partial class FileSystemEnumerator<TResult> : CriticalFinalizerObject, IEnumerator<TResult>
{
// The largest supported path on Unix is 4K bytes of UTF-8 (most only support 1K)
private const int StandardBufferSize = 4096;
private readonly string _originalRootDirectory;
private readonly string _rootDirectory;
private readonly EnumerationOptions _options;
private readonly object _lock = new object();
private string? _currentPath;
private IntPtr _directoryHandle;
private bool _lastEntryFound;
private Queue<(string Path, int RemainingDepth)>? _pending;
private Interop.Sys.DirectoryEntry _entry;
private TResult? _current;
// Used for creating full paths
private char[]? _pathBuffer;
// Used to get the raw entry data
private byte[]? _entryBuffer;
private void Init()
{
// We need to initialize the directory handle up front to ensure
// we immediately throw IO exceptions for missing directory/etc.
_directoryHandle = CreateDirectoryHandle(_rootDirectory);
if (_directoryHandle == IntPtr.Zero)
_lastEntryFound = true;
_currentPath = _rootDirectory;
try
{
_pathBuffer = ArrayPool<char>.Shared.Rent(StandardBufferSize);
int size = Interop.Sys.GetReadDirRBufferSize();
_entryBuffer = size > 0 ? ArrayPool<byte>.Shared.Rent(size) : null;
}
catch
{
// Close the directory handle right away if we fail to allocate
CloseDirectoryHandle();
throw;
}
}
private bool InternalContinueOnError(Interop.ErrorInfo info, bool ignoreNotFound = false)
=> (ignoreNotFound && IsDirectoryNotFound(info)) || (_options.IgnoreInaccessible && IsAccessError(info)) || ContinueOnError(info.RawErrno);
private static bool IsDirectoryNotFound(Interop.ErrorInfo info)
=> info.Error == Interop.Error.ENOTDIR || info.Error == Interop.Error.ENOENT;
private static bool IsAccessError(Interop.ErrorInfo info)
=> info.Error == Interop.Error.EACCES || info.Error == Interop.Error.EBADF
|| info.Error == Interop.Error.EPERM;
private IntPtr CreateDirectoryHandle(string path, bool ignoreNotFound = false)
{
IntPtr handle = Interop.Sys.OpenDir(path);
if (handle == IntPtr.Zero)
{
Interop.ErrorInfo info = Interop.Sys.GetLastErrorInfo();
if (InternalContinueOnError(info, ignoreNotFound))
{
return IntPtr.Zero;
}
throw Interop.GetExceptionForIoErrno(info, path, isDirError: true);
}
return handle;
}
private void CloseDirectoryHandle()
{
IntPtr handle = Interlocked.Exchange(ref _directoryHandle, IntPtr.Zero);
if (handle != IntPtr.Zero)
Interop.Sys.CloseDir(handle);
}
public bool MoveNext()
{
if (_lastEntryFound)
return false;
FileSystemEntry entry = default;
lock (_lock)
{
if (_lastEntryFound)
return false;
// If HAVE_READDIR_R is defined for the platform FindNextEntry depends on _entryBuffer being fixed since
// _entry will point to a string in the middle of the array. If the array is not fixed GC can move it after
// the native call and _entry will point to a bogus file name.
fixed (byte* entryBufferPtr = _entryBuffer)
{
do
{
FindNextEntry(entryBufferPtr, _entryBuffer == null ? 0 : _entryBuffer.Length);
if (_lastEntryFound)
return false;
FileAttributes attributes = FileSystemEntry.Initialize(
ref entry, _entry, _currentPath, _rootDirectory, _originalRootDirectory, new Span<char>(_pathBuffer));
bool isDirectory = (attributes & FileAttributes.Directory) != 0;
bool isSymlink = (attributes & FileAttributes.ReparsePoint) != 0;
bool isSpecialDirectory = false;
if (isDirectory)
{
// Subdirectory found
if (_entry.Name[0] == '.' && (_entry.Name[1] == 0 || (_entry.Name[1] == '.' && _entry.Name[2] == 0)))
{
// "." or "..", don't process unless the option is set
if (!_options.ReturnSpecialDirectories)
continue;
isSpecialDirectory = true;
}
}
if (!isSpecialDirectory && _options.AttributesToSkip != 0)
{
// entry.IsHidden and entry.IsReadOnly will hit the disk if the caches had not been
// initialized yet and we could not soft-retrieve the attributes in Initialize
if ((ShouldSkip(FileAttributes.Directory) && isDirectory) ||
(ShouldSkip(FileAttributes.ReparsePoint) && isSymlink) ||
(ShouldSkip(FileAttributes.Hidden) && entry.IsHidden) ||
(ShouldSkip(FileAttributes.ReadOnly) && entry.IsReadOnly))
{
continue;
}
}
if (isDirectory && !isSpecialDirectory)
{
if (_options.RecurseSubdirectories && _remainingRecursionDepth > 0 && ShouldRecurseIntoEntry(ref entry))
{
// Recursion is on and the directory was accepted, Queue it
_pending ??= new Queue<(string Path, int RemainingDepth)>();
_pending.Enqueue((Path.Join(_currentPath, entry.FileName), _remainingRecursionDepth - 1));
}
}
if (ShouldIncludeEntry(ref entry))
{
_current = TransformEntry(ref entry);
return true;
}
} while (true);
}
}
bool ShouldSkip(FileAttributes attributeToSkip) => (_options.AttributesToSkip & attributeToSkip) != 0;
}
private unsafe void FindNextEntry()
{
fixed (byte* entryBufferPtr = _entryBuffer)
{
FindNextEntry(entryBufferPtr, _entryBuffer == null ? 0 : _entryBuffer.Length);
}
}
private unsafe void FindNextEntry(byte* entryBufferPtr, int bufferLength)
{
int result;
fixed (Interop.Sys.DirectoryEntry* e = &_entry)
{
result = Interop.Sys.ReadDirR(_directoryHandle, entryBufferPtr, bufferLength, e);
}
switch (result)
{
case -1:
// End of directory
DirectoryFinished();
break;
case 0:
// Success
break;
default:
// Error
if (InternalContinueOnError(new Interop.ErrorInfo(result)))
{
DirectoryFinished();
break;
}
else
{
throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(result), _currentPath, isDirError: true);
}
}
}
private bool DequeueNextDirectory()
{
// In Windows we open handles before we queue them, not after. If we fail to create the handle
// but are ok with it (IntPtr.Zero), we don't queue them. Unix can't handle having a lot of
// open handles, so we open after the fact.
//
// Doing the same on Windows would create a performance hit as we would no longer have the context
// of the parent handle to open from. Keeping the parent handle open would increase the amount of
// data we're maintaining, the number of active handles (they're not infinite), and the length
// of time we have handles open (preventing some actions such as renaming/deleting/etc.).
_directoryHandle = IntPtr.Zero;
while (_directoryHandle == IntPtr.Zero)
{
if (_pending == null || _pending.Count == 0)
return false;
(_currentPath, _remainingRecursionDepth) = _pending.Dequeue();
_directoryHandle = CreateDirectoryHandle(_currentPath, ignoreNotFound: true);
}
return true;
}
private void InternalDispose(bool disposing)
{
// It is possible to fail to allocate the lock, but the finalizer will still run
if (_lock != null)
{
lock (_lock)
{
_lastEntryFound = true;
_pending = null;
CloseDirectoryHandle();
if (_pathBuffer is char[] pathBuffer)
{
_pathBuffer = null;
ArrayPool<char>.Shared.Return(pathBuffer);
}
if (_entryBuffer is byte[] entryBuffer)
{
_entryBuffer = null;
ArrayPool<byte>.Shared.Return(entryBuffer);
}
}
}
Dispose(disposing);
}
}
}