File: src\libraries\System.Private.CoreLib\src\System\IO\Enumeration\FileSystemEnumerator.Unix.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// 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 != FileAttributes.None)
                        {
                            // 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);
        }
    }
}