Reduce Unix enumeration allocations (dotnet/corefx#26942)
authorJeremy Kuhne <jeremy.kuhne@microsoft.com>
Thu, 8 Feb 2018 20:55:15 +0000 (12:55 -0800)
committerGitHub <noreply@github.com>
Thu, 8 Feb 2018 20:55:15 +0000 (12:55 -0800)
* Reduce Unix enumeration allocations

This change factors out the FileStatus access into a helper struct and adds overloads
for the stat imports  that allow passing a span.

The next steps:

- Handle errors manually
- Skip using safehandle
- Look for further opportunities around UTF-8/16 conversion

* Address feedback

Shuffle code around a bit in FileInfo for clarity. Also shift directory determination
to FileSystemEntry to avoid allocation on links.

Commit migrated from https://github.com/dotnet/corefx/commit/c64171e379090b372fe232ec3f27a57ae838406c

16 files changed:
src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs [new file with mode: 0644]
src/libraries/Common/src/System/IO/PathInternal.cs
src/libraries/Common/src/System/Text/ValueUtf8Converter.cs [new file with mode: 0644]
src/libraries/System.IO.FileSystem/System.IO.FileSystem.sln
src/libraries/System.IO.FileSystem/src/System.IO.FileSystem.csproj
src/libraries/System.IO.FileSystem/src/System/IO/DirectoryInfo.Unix.cs [new file with mode: 0644]
src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs
src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Unix.cs
src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Windows.cs
src/libraries/System.IO.FileSystem/src/System/IO/FileInfo.Unix.cs [new file with mode: 0644]
src/libraries/System.IO.FileSystem/src/System/IO/FileStatus.Unix.cs [new file with mode: 0644]
src/libraries/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs
src/libraries/System.IO.FileSystem/src/System/IO/FileSystemInfo.Win32.cs [deleted file]
src/libraries/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs
src/libraries/System.IO.FileSystem/src/System/IO/FileSystemInfo.cs
src/libraries/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.netcoreapp.cs

diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs
new file mode 100644 (file)
index 0000000..6216a6b
--- /dev/null
@@ -0,0 +1,41 @@
+// 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;
+using System.Runtime.InteropServices;
+using System.Text;
+
+internal static partial class Interop
+{
+    internal static partial class Sys
+    {
+        // Unix max paths are typically 1K or 4K UTF-8 bytes, 256 should handle the majority of paths
+        // without putting too much pressure on the stack.
+        private const int StackBufferSize = 256;
+
+        [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_Stat2", SetLastError = true)]
+        internal unsafe static extern int Stat(ref byte path, out FileStatus output);
+
+        internal unsafe static int Stat(ReadOnlySpan<char> path, out FileStatus output)
+        {
+            byte* buffer = stackalloc byte[StackBufferSize];
+            var converter = new ValueUtf8Converter(new Span<byte>(buffer, StackBufferSize));
+            int result = Stat(ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), out output);
+            converter.Dispose();
+            return result;
+        }
+
+        [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_LStat2", SetLastError = true)]
+        internal static extern int LStat(ref byte path, out FileStatus output);
+
+        internal unsafe static int LStat(ReadOnlySpan<char> path, out FileStatus output)
+        {
+            byte* buffer = stackalloc byte[StackBufferSize];
+            var converter = new ValueUtf8Converter(new Span<byte>(buffer, StackBufferSize));
+            int result = LStat(ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), out output);
+            converter.Dispose();
+            return result;
+        }
+    }
+}
index 4d221e0..c8135cd 100644 (file)
@@ -70,8 +70,8 @@ namespace System.IO
         /// <summary>
         /// Returns true if the path ends in a directory separator.
         /// </summary>
-        internal static bool EndsInDirectorySeparator(string path) =>
-            !string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]);
+        internal static bool EndsInDirectorySeparator(ReadOnlySpan<char> path) =>
+            path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]);
 
         /// <summary>
         /// Get the common path length from the start of the string.
diff --git a/src/libraries/Common/src/System/Text/ValueUtf8Converter.cs b/src/libraries/Common/src/System/Text/ValueUtf8Converter.cs
new file mode 100644 (file)
index 0000000..3a2ba29
--- /dev/null
@@ -0,0 +1,51 @@
+// 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.Buffers;
+
+namespace System.Text
+{
+    /// <summary>
+    /// Helper to allow utilizing stack buffer for conversion to UTF-8. Will
+    /// switch to ArrayPool if not given enough memory. As such, make sure to
+    /// call Clear() to return any potentially rented buffer after conversion.
+    /// </summary>
+    internal ref struct ValueUtf8Converter
+    {
+        private byte[] _arrayToReturnToPool;
+        private Span<byte> _bytes;
+
+        public ValueUtf8Converter(Span<byte> initialBuffer)
+        {
+            _arrayToReturnToPool = null;
+            _bytes = initialBuffer;
+        }
+
+        public Span<byte> ConvertAndTerminateString(ReadOnlySpan<char> value)
+        {
+            int maxSize = Encoding.UTF8.GetMaxByteCount(value.Length) + 1;
+            if (_bytes.Length < maxSize)
+            {
+                Dispose();
+                _arrayToReturnToPool = ArrayPool<byte>.Shared.Rent(maxSize);
+                _bytes = new Span<byte>(_arrayToReturnToPool);
+            }
+
+            // Grab the bytes and null terminate
+            int byteCount = Encoding.UTF8.GetBytes(value, _bytes);
+            _bytes[byteCount] = 0;
+            return _bytes.Slice(0, byteCount + 1);
+        }
+
+        public void Dispose()
+        {
+            byte[] toReturn = _arrayToReturnToPool;
+            if (toReturn != null)
+            {
+                _arrayToReturnToPool = null;
+                ArrayPool<byte>.Shared.Return(toReturn);
+            }
+        }
+    }
+}
index 7141b20..7da304a 100644 (file)
@@ -1,6 +1,6 @@
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25420.1
+# Visual Studio 15
+VisualStudioVersion = 15.0.27130.2027
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.IO.FileSystem.Tests", "tests\System.IO.FileSystem.Tests.csproj", "{F0D49126-6A1C-42D5-9428-4374C868BAF8}"
        ProjectSection(ProjectDependencies) = postProject
@@ -39,8 +39,8 @@ Global
                {3C42F714-82AF-4A43-9B9C-744DE31B5C5D}.Debug|Any CPU.Build.0 = netstandard-Debug|Any CPU
                {3C42F714-82AF-4A43-9B9C-744DE31B5C5D}.Release|Any CPU.ActiveCfg = netstandard-Release|Any CPU
                {3C42F714-82AF-4A43-9B9C-744DE31B5C5D}.Release|Any CPU.Build.0 = netstandard-Release|Any CPU
-               {1B528B61-14F9-4BFC-A79A-F0BDB3339150}.Debug|Any CPU.ActiveCfg = netcoreapp-Windows_NT-Debug|Any CPU
-               {1B528B61-14F9-4BFC-A79A-F0BDB3339150}.Debug|Any CPU.Build.0 = netcoreapp-Windows_NT-Debug|Any CPU
+               {1B528B61-14F9-4BFC-A79A-F0BDB3339150}.Debug|Any CPU.ActiveCfg = netcoreapp-Unix-Debug|Any CPU
+               {1B528B61-14F9-4BFC-A79A-F0BDB3339150}.Debug|Any CPU.Build.0 = netcoreapp-Unix-Debug|Any CPU
                {1B528B61-14F9-4BFC-A79A-F0BDB3339150}.Release|Any CPU.ActiveCfg = netcoreapp-Windows_NT-Release|Any CPU
                {1B528B61-14F9-4BFC-A79A-F0BDB3339150}.Release|Any CPU.Build.0 = netcoreapp-Windows_NT-Release|Any CPU
                {4B15C12E-B6AB-4B05-8ECA-C2E2AEA67482}.Debug|Any CPU.ActiveCfg = netcoreapp-Debug|Any CPU
@@ -57,4 +57,7 @@ Global
                {1B528B61-14F9-4BFC-A79A-F0BDB3339150} = {E107E9C1-E893-4E87-987E-04EF0DCEAEFD}
                {4B15C12E-B6AB-4B05-8ECA-C2E2AEA67482} = {2E666815-2EDB-464B-9DF6-380BF4789AD4}
        EndGlobalSection
+       GlobalSection(ExtensibilityGlobals) = postSolution
+               SolutionGuid = {28498879-453E-42C1-8D74-0F9F80B1B58E}
+       EndGlobalSection
 EndGlobal
index 257f089..ba85317 100644 (file)
     <Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.GET_FILEEX_INFO_LEVELS.cs">
       <Link>Common\Interop\Windows\Interop.GET_FILEEX_INFO_LEVELS.cs</Link>
     </Compile>
-    <Compile Include="System\IO\FileSystemInfo.Win32.cs" />
     <Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.SetThreadErrorMode.cs">
       <Link>Common\Interop\Windows\Interop.SetThreadErrorMode.cs</Link>
     </Compile>
   </ItemGroup>
   <!-- Unix -->
   <ItemGroup Condition="'$(TargetsUnix)' == 'true'">
+    <Compile Include="System\IO\FileStatus.Unix.cs" />
     <Compile Include="System\IO\Enumeration\FileSystemEntry.Unix.cs" />
     <Compile Include="System\IO\Enumeration\FileSystemEnumerator.Unix.cs" />
     <Compile Include="System\IO\CharSpanExtensions.Unix.cs" />
+    <Compile Include="System\IO\FileInfo.Unix.cs" />
+    <Compile Include="System\IO\DirectoryInfo.Unix.cs" />
     <Compile Include="System\IO\FileSystemInfo.Unix.cs" />
     <Compile Include="System\IO\PathHelpers.Unix.cs" />
     <Compile Include="System\IO\FileSystem.Unix.cs" />
     <Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.Stat.cs">
       <Link>Common\Interop\Unix\Interop.Stat.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.Stat.Span.cs">
+      <Link>Common\Interop\Unix\Interop.Stat.Span.cs</Link>
+    </Compile>
     <Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.ReadDir.cs">
       <Link>Common\Interop\Unix\Interop.ReadDir.cs</Link>
     </Compile>
     <Compile Include="$(CommonPath)\System\IO\DriveInfoInternal.Unix.cs">
       <Link>Common\System\IO\DriveInfoInternal.Unix.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)\System\Text\ValueUtf8Converter.cs">
+      <Link>Common\System\Text\ValueUtf8Converter.cs</Link>
+    </Compile>
   </ItemGroup>
   <ItemGroup>
     <Reference Include="System.Buffers" />
diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/DirectoryInfo.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/DirectoryInfo.Unix.cs
new file mode 100644 (file)
index 0000000..307df6c
--- /dev/null
@@ -0,0 +1,16 @@
+// 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.
+
+namespace System.IO
+{
+    partial class DirectoryInfo
+    {
+        internal static unsafe DirectoryInfo Create(string fullPath, string fileName, ref FileStatus fileStatus)
+        {
+            DirectoryInfo info = new DirectoryInfo(fullPath, fileName: fileName, isNormalized: true);
+            info.Init(ref fileStatus);
+            return info;
+        }
+    }
+}
index a4645cd..d142879 100644 (file)
@@ -9,41 +9,60 @@ namespace System.IO.Enumeration
     /// </summary>
     public unsafe ref struct FileSystemEntry
     {
-        // TODO: Unix implementation https://github.com/dotnet/corefx/issues/26715
-        // Inital implementation is naive and not optimized.
-
-        internal static void Initialize(
+        internal static bool Initialize(
             ref FileSystemEntry entry,
             Interop.Sys.DirectoryEntry directoryEntry,
-            bool isDirectory,
             ReadOnlySpan<char> directory,
             string rootDirectory,
-            string originalRootDirectory)
+            string originalRootDirectory,
+            Span<char> pathBuffer)
         {
             entry._directoryEntry = directoryEntry;
-            entry._isDirectory = isDirectory;
             entry.Directory = directory;
             entry.RootDirectory = rootDirectory;
             entry.OriginalRootDirectory = originalRootDirectory;
+            entry._pathBuffer = pathBuffer;
+
+            // Get from the dir entry whether the entry is a file or directory.
+            // We classify everything as a file unless we know it to be a directory.
+            // (This includes regular files, FIFOs, etc.)
+
+            bool isDirectory = false;
+            if (directoryEntry.InodeType == Interop.Sys.NodeType.DT_DIR)
+            {
+                // We know it's a directory.
+                isDirectory = true;
+            }
+            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)
+            {
+                // It's a 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;
+            }
+
+            FileStatus.Initialize(ref entry._status, isDirectory);
+            return isDirectory;
         }
 
         internal Interop.Sys.DirectoryEntry _directoryEntry;
-        private FileSystemInfo _info;
-        private bool _isDirectory;
+        private FileStatus _status;
+        private Span<char> _pathBuffer;
+        private ReadOnlySpan<char> _fullPath;
 
-        private FileSystemInfo Info
+        private ReadOnlySpan<char> FullPath
         {
             get
             {
-                if (_info == null)
+                if (_fullPath.Length == 0)
                 {
-                    string fullPath = PathHelpers.CombineNoChecks(Directory, _directoryEntry.InodeName);
-                    _info = _isDirectory
-                        ? (FileSystemInfo) new DirectoryInfo(fullPath, fullPath, _directoryEntry.InodeName, isNormalized: true)
-                        : new FileInfo(fullPath, fullPath, _directoryEntry.InodeName, isNormalized: true);
-                    _info.Refresh();
+                    ReadOnlySpan<char> directory = Directory;
+                    directory.CopyTo(_pathBuffer);
+                    _pathBuffer[directory.Length] = Path.DirectorySeparatorChar;
+                    ReadOnlySpan<char> fileName = _directoryEntry.InodeName;
+                    fileName.CopyTo(_pathBuffer.Slice(directory.Length + 1));
+                    _fullPath = _pathBuffer.Slice(0, directory.Length + 1 + fileName.Length);
                 }
-                return _info;
+                return _fullPath;
             }
         }
 
@@ -63,13 +82,25 @@ namespace System.IO.Enumeration
         public string OriginalRootDirectory { get; private set; }
 
         public ReadOnlySpan<char> FileName => _directoryEntry.InodeName;
-        public FileAttributes Attributes => Info.Attributes;
-        public long Length => Info.LengthCore;
-        public DateTimeOffset CreationTimeUtc => Info.CreationTimeCore;
-        public DateTimeOffset LastAccessTimeUtc => Info.LastAccessTimeCore;
-        public DateTimeOffset LastWriteTimeUtc => Info.LastWriteTimeCore;
-        public bool IsDirectory => _isDirectory;
-        public FileSystemInfo ToFileSystemInfo() => Info;
+        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);
+        public bool IsDirectory => _status.InitiallyDirectory;
+
+        public FileSystemInfo ToFileSystemInfo()
+        {
+            string fullPath = ToFullPath();
+            if (_status.InitiallyDirectory)
+            {
+                return DirectoryInfo.Create(fullPath, _directoryEntry.InodeName, ref _status);
+            }
+            else
+            {
+                return FileInfo.Create(fullPath, _directoryEntry.InodeName, ref _status);
+            }
+        }
 
         /// <summary>
         /// Returns the full path for find results, based on the initially provided path.
@@ -81,6 +112,6 @@ namespace System.IO.Enumeration
         /// Returns the full path of the find result.
         /// </summary>
         public string ToFullPath() =>
-            PathHelpers.CombineNoChecks(Directory, FileName);
+            new string(FullPath);
     }
 }
index eaadea9..616e169 100644 (file)
@@ -2,15 +2,18 @@
 // 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.Buffers;
 using System.Collections.Generic;
 using System.Runtime.ConstrainedExecution;
-using System.Threading;
 using Microsoft.Win32.SafeHandles;
 
 namespace System.IO.Enumeration
 {
     public unsafe abstract 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;
@@ -25,6 +28,9 @@ namespace System.IO.Enumeration
         private Interop.Sys.DirectoryEntry _entry;
         private TResult _current;
 
+        // Used for creating full paths
+        private char[] _pathBuffer;
+
         /// <summary>
         /// Encapsulates a find operation.
         /// </summary>
@@ -33,7 +39,7 @@ namespace System.IO.Enumeration
         public FileSystemEnumerator(string directory, EnumerationOptions options = null)
         {
             _originalRootDirectory = directory ?? throw new ArgumentNullException(nameof(directory));
-            _rootDirectory = Path.GetFullPath(directory);
+            _rootDirectory = Path.GetFullPath(directory).TrimEnd(Path.DirectorySeparatorChar);
             _options = options ?? EnumerationOptions.Default;
 
             // We need to initialize the directory handle up front to ensure
@@ -43,6 +49,17 @@ namespace System.IO.Enumeration
                 _lastEntryFound = true;
 
             _currentPath = _rootDirectory;
+
+            try
+            {
+                _pathBuffer = ArrayPool<char>.Shared.Rent(StandardBufferSize);
+            }
+            catch
+            {
+                // Close the directory handle right away if we fail to allocate
+                CloseDirectoryHandle();
+                throw;
+            }
         }
 
         private static SafeDirectoryHandle CreateDirectoryHandle(string path)
@@ -50,7 +67,7 @@ namespace System.IO.Enumeration
             // TODO: https://github.com/dotnet/corefx/issues/26715
             // - Check access denied option and allow through if specified.
             // - Use IntPtr handle directly
-            Microsoft.Win32.SafeHandles.SafeDirectoryHandle handle = Interop.Sys.OpenDir(path);
+            SafeDirectoryHandle handle = Interop.Sys.OpenDir(path);
             if (handle.IsInvalid)
             {
                 throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), path, isDirectory: true);
@@ -82,35 +99,25 @@ namespace System.IO.Enumeration
                     if (_lastEntryFound)
                         return false;
 
-                    // Get from the dir entry whether the entry is a file or directory.
-                    // We classify everything as a file unless we know it to be a directory.
-                    // (This includes regular files, FIFOs, etc.)
-                    bool isDirectory = false;
-                    if (_entry.InodeType == Interop.Sys.NodeType.DT_DIR)
-                    {
-                        // We know it's a directory.
-                        isDirectory = true;
-                    }
-                    else if (_entry.InodeType == Interop.Sys.NodeType.DT_LNK || _entry.InodeType == Interop.Sys.NodeType.DT_UNKNOWN)
-                    {
-                        // It's a symlink or unknown: stat to it to see if we can resolve it to a directory.
-                        // If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file.
-                        isDirectory = FileSystem.DirectoryExists(Path.Combine(_currentPath, _entry.InodeName));
-                    }
+                    bool isDirectory = FileSystemEntry.Initialize(ref entry, _entry, _currentPath, _rootDirectory, _originalRootDirectory, new Span<char>(_pathBuffer));
 
                     if (_options.AttributesToSkip != 0)
                     {
-                        if (((_options.AttributesToSkip & FileAttributes.Directory) != 0 && isDirectory)
-                            || ((_options.AttributesToSkip & FileAttributes.Hidden) != 0 && _entry.InodeName[0] == '.')
-                            || ((_options.AttributesToSkip & FileAttributes.ReparsePoint) != 0 && _entry.InodeType == Interop.Sys.NodeType.DT_LNK))
+                        if ((_options.AttributesToSkip & ~(FileAttributes.Directory | FileAttributes.Hidden | FileAttributes.ReparsePoint)) == 0)
+                        {
+                            // These three we don't have to hit the disk again to evaluate
+                            if (((_options.AttributesToSkip & FileAttributes.Directory) != 0 && isDirectory)
+                                || ((_options.AttributesToSkip & FileAttributes.Hidden) != 0 && _entry.InodeName[0] == '.')
+                                || ((_options.AttributesToSkip & FileAttributes.ReparsePoint) != 0 && _entry.InodeType == Interop.Sys.NodeType.DT_LNK))
+                                continue;
+                        }
+                        else if ((_options.AttributesToSkip & entry.Attributes) != 0)
+                        {
+                            // Hitting Attributes on the FileSystemEntry will cause a stat call
                             continue;
-
-                        // TODO: https://github.com/dotnet/corefx/issues/26715
-                        // Handle readonly skipping
+                        }
                     }
 
-                    FileSystemEntry.Initialize(ref entry, _entry, isDirectory, _currentPath, _rootDirectory, _originalRootDirectory);
-
                     if (isDirectory)
                     {
                         // Subdirectory found
index 18fa129..3d8ed36 100644 (file)
@@ -43,7 +43,7 @@ namespace System.IO.Enumeration
         public FileSystemEnumerator(string directory, EnumerationOptions options = null)
         {
             _originalRootDirectory = directory ?? throw new ArgumentNullException(nameof(directory));
-            _rootDirectory = Path.GetFullPath(directory);
+            _rootDirectory = Path.GetFullPath(directory).TrimEnd(Path.DirectorySeparatorChar);
             _options = options ?? EnumerationOptions.Default;
 
             // We'll only suppress the media insertion prompt on the topmost directory as that is the
diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/FileInfo.Unix.cs b/src/libraries/System.IO.FileSystem/src/System/IO/FileInfo.Unix.cs
new file mode 100644 (file)
index 0000000..e88bb9a
--- /dev/null
@@ -0,0 +1,16 @@
+// 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.
+
+namespace System.IO
+{
+    partial class FileInfo
+    {
+        internal static unsafe FileInfo Create(string fullPath, string fileName, ref FileStatus fileStatus)
+        {
+            FileInfo info = new FileInfo(fullPath, fileName: fileName, isNormalized: true);
+            info.Init(ref fileStatus);
+            return info;
+        }
+    }
+}
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
new file mode 100644 (file)
index 0000000..4feff64
--- /dev/null
@@ -0,0 +1,280 @@
+// 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.
+
+namespace System.IO
+{
+    internal struct FileStatus
+    {
+        private const int NanosecondsPerTick = 100;
+
+        // The last cached stat information about the file
+        private Interop.Sys.FileStatus _fileStatus;
+
+        // -1 if _fileStatus isn't initialized, 0 if _fileStatus was initialized with no
+        // errors, or the errno error code.
+        private int _fileStatusInitialized;
+
+        // We track intent of creation to know whether or not we want to (1) create a
+        // DirectoryInfo around this status struct or (2) actually are part of a DirectoryInfo.
+        internal bool InitiallyDirectory { get; private set; }
+
+        // Is a directory as of the last refresh
+        internal bool _isDirectory;
+
+        // Exists as of the last refresh
+        private bool _exists;
+
+        internal static void Initialize(
+            ref FileStatus status,
+            bool isDirectory)
+        {
+            status.InitiallyDirectory = isDirectory;
+            status._fileStatusInitialized = -1;
+        }
+
+        internal void Invalidate() => _fileStatusInitialized = -1;
+
+        public FileAttributes GetAttributes(ReadOnlySpan<char> path, ReadOnlySpan<char> fileName)
+        {
+            EnsureStatInitialized(path);
+
+            if (!_exists)
+                return (FileAttributes)(-1);
+
+            FileAttributes attrs = default;
+
+            bool IsReadOnly(ref Interop.Sys.FileStatus fileStatus)
+            {
+                Interop.Sys.Permissions readBit, writeBit;
+                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())
+                {
+                    // User belongs to a group that effectively owns the file
+                    readBit = Interop.Sys.Permissions.S_IRGRP;
+                    writeBit = Interop.Sys.Permissions.S_IWGRP;
+                }
+                else
+                {
+                    // Others permissions
+                    readBit = Interop.Sys.Permissions.S_IROTH;
+                    writeBit = Interop.Sys.Permissions.S_IWOTH;
+                }
+
+                return
+                    (fileStatus.Mode & (int)readBit) != 0 && // has read permission
+                    (fileStatus.Mode & (int)writeBit) == 0;  // but not write permission
+            }
+
+            if (_isDirectory) // this is the one attribute where we follow symlinks
+            {
+                attrs |= FileAttributes.Directory;
+            }
+            if (IsReadOnly(ref _fileStatus))
+            {
+                attrs |= FileAttributes.ReadOnly;
+            }
+            if ((_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK)
+            {
+                attrs |= FileAttributes.ReparsePoint;
+            }
+
+            // If the filename starts with a period, it's hidden.
+            if (fileName.Length > 0 && fileName[0] == '.')
+            {
+                attrs |= FileAttributes.Hidden;
+            }
+
+            return attrs != default ? attrs : FileAttributes.Normal;
+        }
+
+        public void SetAttributes(string path, FileAttributes attributes)
+        {
+            // Validate that only flags from the attribute are being provided.  This is an
+            // approximation for the validation done by the Win32 function.
+            const FileAttributes allValidFlags =
+                FileAttributes.Archive | FileAttributes.Compressed | FileAttributes.Device |
+                FileAttributes.Directory | FileAttributes.Encrypted | FileAttributes.Hidden |
+                FileAttributes.Hidden | FileAttributes.IntegrityStream | FileAttributes.Normal |
+                FileAttributes.NoScrubData | FileAttributes.NotContentIndexed | FileAttributes.Offline |
+                FileAttributes.ReadOnly | FileAttributes.ReparsePoint | FileAttributes.SparseFile |
+                FileAttributes.System | FileAttributes.Temporary;
+            if ((attributes & ~allValidFlags) != 0)
+            {
+                // Using constant string for argument to match historical throw
+                throw new ArgumentException(SR.Arg_InvalidFileAttrs, "Attributes");
+            }
+
+            EnsureStatInitialized(path);
+
+            if (!_exists)
+                FileSystemInfo.ThrowNotFound(path);
+
+            // The only thing we can reasonably change is whether the file object is readonly by changing permissions.
+
+            int newMode = _fileStatus.Mode;
+            if ((attributes & FileAttributes.ReadOnly) != 0)
+            {
+                // Take away all write permissions from user/group/everyone
+                newMode &= ~(int)(Interop.Sys.Permissions.S_IWUSR | Interop.Sys.Permissions.S_IWGRP | Interop.Sys.Permissions.S_IWOTH);
+            }
+            else if ((newMode & (int)Interop.Sys.Permissions.S_IRUSR) != 0)
+            {
+                // Give write permission to the owner if the owner has read permission
+                newMode |= (int)Interop.Sys.Permissions.S_IWUSR;
+            }
+
+            // Change the permissions on the file
+            if (newMode != _fileStatus.Mode)
+            {
+                Interop.CheckIo(Interop.Sys.ChMod(path, newMode), path, InitiallyDirectory);
+            }
+
+            _fileStatusInitialized = -1;
+        }
+
+        internal bool GetExists(ReadOnlySpan<char> path)
+        {
+            if (_fileStatusInitialized == -1)
+                Refresh(path);
+
+            return _exists && InitiallyDirectory == _isDirectory;
+        }
+
+        internal DateTimeOffset GetCreationTime(ReadOnlySpan<char> path)
+        {
+            EnsureStatInitialized(path);
+            if (!_exists)
+                return DateTimeOffset.FromFileTime(0);
+
+            if ((_fileStatus.Flags & Interop.Sys.FileStatusFlags.HasBirthTime) != 0)
+                return UnixTimeToDateTimeOffset(_fileStatus.BirthTime, _fileStatus.BirthTimeNsec);
+
+            // fall back to the oldest time we have in between change and modify time
+            if (_fileStatus.MTime < _fileStatus.CTime ||
+                (_fileStatus.MTime == _fileStatus.CTime && _fileStatus.MTimeNsec < _fileStatus.CTimeNsec))
+                return UnixTimeToDateTimeOffset(_fileStatus.MTime, _fileStatus.MTimeNsec);
+
+            return UnixTimeToDateTimeOffset(_fileStatus.CTime, _fileStatus.CTimeNsec);
+        }
+
+        internal void SetCreationTime(string path, DateTimeOffset time)
+        {
+            // There isn't a reliable way to set this; however, we can't just do nothing since the
+            // FileSystemWatcher specifically looks for this call to make a Metadata Change, so we
+            // should set the LastAccessTime of the file to cause the metadata change we need.
+            SetLastAccessTime(path, time);
+        }
+
+        internal DateTimeOffset GetLastAccessTime(ReadOnlySpan<char> path)
+        {
+            EnsureStatInitialized(path);
+            if (!_exists)
+                return DateTimeOffset.FromFileTime(0);
+            return UnixTimeToDateTimeOffset(_fileStatus.ATime, _fileStatus.ATimeNsec);
+        }
+
+        internal void SetLastAccessTime(string path, DateTimeOffset time)
+            => SetAccessWriteTimes(path, time.ToUnixTimeSeconds(), null);
+
+        internal DateTimeOffset GetLastWriteTime(ReadOnlySpan<char> path)
+        {
+            EnsureStatInitialized(path);
+            if (!_exists)
+                return DateTimeOffset.FromFileTime(0);
+            return UnixTimeToDateTimeOffset(_fileStatus.MTime, _fileStatus.MTimeNsec);
+        }
+
+        internal void SetLastWriteTime(string path, DateTimeOffset time)
+            => SetAccessWriteTimes(path, null, time.ToUnixTimeSeconds());
+
+        private DateTimeOffset UnixTimeToDateTimeOffset(long seconds, long nanoseconds)
+        {
+            return DateTimeOffset.FromUnixTimeSeconds(seconds).AddTicks(nanoseconds / NanosecondsPerTick).ToLocalTime();
+        }
+
+        private void SetAccessWriteTimes(string path, long? accessTime, long? writeTime)
+        {
+            // force a refresh so that we have an up-to-date times for values not being overwritten
+            _fileStatusInitialized = -1;
+            EnsureStatInitialized(path);
+            Interop.Sys.UTimBuf buf;
+            // we use utime() not utimensat() so we drop the subsecond part
+            buf.AcTime = accessTime ?? _fileStatus.ATime;
+            buf.ModTime = writeTime ?? _fileStatus.MTime;
+            Interop.CheckIo(Interop.Sys.UTime(path, ref buf), path, InitiallyDirectory);
+            _fileStatusInitialized = -1;
+        }
+
+        internal long GetLength(ReadOnlySpan<char> path)
+        {
+            EnsureStatInitialized(path);
+            return _fileStatus.Size;
+        }
+
+        public void Refresh(ReadOnlySpan<char> path)
+        {
+            // This should not throw, instead we store the result so that we can throw it
+            // when someone actually accesses a property.
+
+            // Use lstat to get the details on the object, without following symlinks.
+            // If it is a symlink, then subsequently get details on the target of the symlink,
+            // storing those results separately.  We only report failure if the initial
+            // lstat fails, as a broken symlink should still report info on exists, attributes, etc.
+            _isDirectory = false;
+            if (PathInternal.EndsInDirectorySeparator(path))
+                path = path.Slice(0, path.Length - 1);
+            int result = Interop.Sys.LStat(path, out _fileStatus);
+            if (result < 0)
+            {
+                Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
+
+                // This should never set the error if the file can't be found.
+                // (see the Windows refresh passing returnErrorOnNotFound: false).
+                if (errorInfo.Error == Interop.Error.ENOENT
+                    || errorInfo.Error == Interop.Error.ENOTDIR)
+                {
+                    _fileStatusInitialized = 0;
+                    _exists = false;
+                }
+                else
+                {
+                    _fileStatusInitialized = errorInfo.RawErrno;
+                }
+                return;
+            }
+
+            _exists = true;
+            _isDirectory = (_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR;
+
+            // 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;
+            }
+
+            _fileStatusInitialized = 0;
+        }
+
+        internal void EnsureStatInitialized(ReadOnlySpan<char> path)
+        {
+            if (_fileStatusInitialized == -1)
+            {
+                Refresh(path);
+            }
+
+            if (_fileStatusInitialized != 0)
+            {
+                int errno = _fileStatusInitialized;
+                _fileStatusInitialized = -1;
+                throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), new string(path));
+            }
+        }
+    }
+}
index 6e3733e..1416544 100644 (file)
@@ -6,325 +6,63 @@ namespace System.IO
 {
     partial class FileSystemInfo
     {
-        private const int NanosecondsPerTick = 100;
+        private FileStatus _fileStatus;
 
-        /// <summary>The last cached stat information about the file.</summary>
-        private Interop.Sys.FileStatus _fileStatus;
-        /// <summary>true if <see cref="_fileStatus"/> represents a symlink and the target of that symlink is a directory.</summary>
-        private bool _targetOfSymlinkIsDirectory;
-
-        /// <summary>
-        /// Exists as a path as of last refresh.
-        /// </summary>
-        private bool _exists;
-
-        /// <summary>
-        /// Whether we've successfully cached a stat structure.
-        /// -1 if we need to refresh _fileStatus, 0 if we've successfully cached one,
-        /// or any other value that serves as an errno error code from the
-        /// last time we tried and failed to refresh _fileStatus.
-        /// </summary>
-        private int _fileStatusInitialized = -1;
-
-        internal void Invalidate()
+        protected FileSystemInfo()
         {
-            _fileStatusInitialized = -1;
+            FileStatus.Initialize(ref _fileStatus, this is DirectoryInfo);
         }
 
-        public FileAttributes Attributes
-        {
-            get
-            {
-                EnsureStatInitialized();
-
-                if (!_exists)
-                    return (FileAttributes)(-1);
-
-                FileAttributes attrs = default(FileAttributes);
-
-                if (IsDirectoryAssumesInitialized) // this is the one attribute where we follow symlinks
-                {
-                    attrs |= FileAttributes.Directory;
-                }
-                if (IsReadOnlyAssumesInitialized)
-                {
-                    attrs |= FileAttributes.ReadOnly;
-                }
-                if (IsSymlinkAssumesInitialized)
-                {
-                    attrs |= FileAttributes.ReparsePoint;
-                }
+        internal void Invalidate() => _fileStatus.Invalidate();
 
-                // If the filename starts with a period, it's hidden. Or if this is a directory ending in a slash,
-                // if the directory name starts with a period, it's hidden.
-                string fileName = Path.GetFileName(FullPath);
-                if (string.IsNullOrEmpty(fileName))
-                {
-                    fileName = Path.GetFileName(Path.GetDirectoryName(FullPath));
-                }
-                if (!string.IsNullOrEmpty(fileName) && fileName[0] == '.')
-                {
-                    attrs |= FileAttributes.Hidden;
-                }
-
-                return attrs != default(FileAttributes) ?
-                    attrs :
-                    FileAttributes.Normal;
-            }
-            set
-            {
-                // Validate that only flags from the attribute are being provided.  This is an
-                // approximation for the validation done by the Win32 function.
-                const FileAttributes allValidFlags =
-                    FileAttributes.Archive | FileAttributes.Compressed | FileAttributes.Device |
-                    FileAttributes.Directory | FileAttributes.Encrypted | FileAttributes.Hidden |
-                    FileAttributes.Hidden | FileAttributes.IntegrityStream | FileAttributes.Normal |
-                    FileAttributes.NoScrubData | FileAttributes.NotContentIndexed | FileAttributes.Offline |
-                    FileAttributes.ReadOnly | FileAttributes.ReparsePoint | FileAttributes.SparseFile |
-                    FileAttributes.System | FileAttributes.Temporary;
-                if ((value & ~allValidFlags) != 0)
-                {
-                    throw new ArgumentException(SR.Arg_InvalidFileAttrs, nameof(value));
-                }
-
-                // The only thing we can reasonably change is whether the file object is readonly,
-                // just changing its permissions accordingly.
-                EnsureStatInitialized();
-
-                if (!_exists)
-                {
-                    ThrowNotFound(FullPath);
-                }
-
-                IsReadOnlyAssumesInitialized = (value & FileAttributes.ReadOnly) != 0;
-                _fileStatusInitialized = -1;
-            }
-        }
-
-        internal static void ThrowNotFound(string path)
+        internal unsafe void Init(ref FileStatus fileStatus)
         {
-            // Windows distinguishes between whether the directory or the file isn't found,
-            // and throws a different exception in these cases.  We attempt to approximate that
-            // here; there is a race condition here, where something could change between
-            // when the error occurs and our checks, but it's the best we can do, and the
-            // worst case in such a race condition (which could occur if the file system is
-            // being manipulated concurrently with these checks) is that we throw a
-            // FileNotFoundException instead of DirectoryNotFoundException.
-
-            bool directoryError = !Directory.Exists(Path.GetDirectoryName(PathHelpers.TrimEndingDirectorySeparator(path)));
-            throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(Interop.Error.ENOENT), path, directoryError);                
+            _fileStatus = fileStatus;
+            _fileStatus.EnsureStatInitialized(FullPath);
         }
 
-        /// <summary>Gets whether stat reported this system object as a directory.</summary>
-        private bool IsDirectoryAssumesInitialized =>
-            (_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR ||
-            (IsSymlinkAssumesInitialized && _targetOfSymlinkIsDirectory);
-
-        /// <summary>Gets whether stat reported this system object as a symlink.</summary>
-        private bool IsSymlinkAssumesInitialized =>
-            (_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK;
-
-        /// <summary>
-        /// Gets or sets whether the file is read-only.  This is based on the read/write/execute
-        /// permissions of the object.
-        /// </summary>
-        private bool IsReadOnlyAssumesInitialized
+        public FileAttributes Attributes
         {
-            get
-            {
-                Interop.Sys.Permissions readBit, writeBit;
-                if (_fileStatus.Uid == Interop.Sys.GetEUid())      // does the user effectively own the file?
-                {
-                    readBit  = Interop.Sys.Permissions.S_IRUSR;
-                    writeBit = Interop.Sys.Permissions.S_IWUSR;
-                }
-                else if (_fileStatus.Gid == Interop.Sys.GetEGid()) // does the user belong to a group that effectively owns the file?
-                {
-                    readBit  = Interop.Sys.Permissions.S_IRGRP;
-                    writeBit = Interop.Sys.Permissions.S_IWGRP;
-                }
-                else                                              // everyone else
-                {
-                    readBit  = Interop.Sys.Permissions.S_IROTH;
-                    writeBit = Interop.Sys.Permissions.S_IWOTH;
-                }
-
-                return
-                    (_fileStatus.Mode & (int)readBit) != 0 && // has read permission
-                    (_fileStatus.Mode & (int)writeBit) == 0;  // but not write permission
-            }
-            set
-            {
-                int newMode = _fileStatus.Mode;
-                if (value) // true if going from writable to readable, false if going from readable to writable
-                {
-                    // Take away all write permissions from user/group/everyone
-                    newMode &= ~(int)(Interop.Sys.Permissions.S_IWUSR | Interop.Sys.Permissions.S_IWGRP | Interop.Sys.Permissions.S_IWOTH);
-                }
-                else if ((newMode & (int)Interop.Sys.Permissions.S_IRUSR) != 0)
-                {
-                    // Give write permission to the owner if the owner has read permission
-                    newMode |= (int)Interop.Sys.Permissions.S_IWUSR;
-                }
-
-                // Change the permissions on the file
-                if (newMode != _fileStatus.Mode)
-                {
-                    bool isDirectory = this is DirectoryInfo;
-                    Interop.CheckIo(Interop.Sys.ChMod(FullPath, newMode), FullPath, isDirectory);
-                }
-            }
+            get => _fileStatus.GetAttributes(FullPath, Name);
+            set => _fileStatus.SetAttributes(FullPath, value);
         }
 
-        internal bool ExistsCore
-        {
-            get
-            {
-                if (_fileStatusInitialized == -1)
-                {
-                    Refresh();
-                }
-
-                return
-                    _exists &&
-                    (this is DirectoryInfo) == IsDirectoryAssumesInitialized;
-            }
-        }
+        internal bool ExistsCore => _fileStatus.GetExists(FullPath);
 
         internal DateTimeOffset CreationTimeCore
         {
-            get
-            {
-                EnsureStatInitialized();
-                if (!_exists)
-                    return DateTimeOffset.FromFileTime(0);
-
-                if ((_fileStatus.Flags & Interop.Sys.FileStatusFlags.HasBirthTime) != 0)
-                    return UnixTimeToDateTimeOffset(_fileStatus.BirthTime, _fileStatus.BirthTimeNsec);
-
-                // fall back to the oldest time we have in between change and modify time
-                if (_fileStatus.MTime < _fileStatus.CTime ||
-                   (_fileStatus.MTime == _fileStatus.CTime && _fileStatus.MTimeNsec < _fileStatus.CTimeNsec))
-                    return UnixTimeToDateTimeOffset(_fileStatus.MTime, _fileStatus.MTimeNsec);
-                
-                return UnixTimeToDateTimeOffset(_fileStatus.CTime, _fileStatus.CTimeNsec);
-            }
-            set
-            {
-                // There isn't a reliable way to set this; however, we can't just do nothing since the
-                // FileSystemWatcher specifically looks for this call to make a Metadata Change, so we
-                // should set the LastAccessTime of the file to cause the metadata change we need.
-                LastAccessTime = LastAccessTime;
-            }
+            get => _fileStatus.GetCreationTime(FullPath);
+            set => _fileStatus.SetCreationTime(FullPath, value);
         }
 
         internal DateTimeOffset LastAccessTimeCore
         {
-            get
-            {
-                EnsureStatInitialized();
-                if (!_exists)
-                    return DateTimeOffset.FromFileTime(0);
-                return UnixTimeToDateTimeOffset(_fileStatus.ATime, _fileStatus.ATimeNsec);
-            }
-            set { SetAccessWriteTimes(value.ToUnixTimeSeconds(), null); }
+            get => _fileStatus.GetLastAccessTime(FullPath);
+            set => _fileStatus.SetLastAccessTime(FullPath, value);
         }
 
         internal DateTimeOffset LastWriteTimeCore
         {
-            get
-            {
-                EnsureStatInitialized();
-                if (!_exists)
-                    return DateTimeOffset.FromFileTime(0);
-                return UnixTimeToDateTimeOffset(_fileStatus.MTime, _fileStatus.MTimeNsec);
-            }
-            set { SetAccessWriteTimes(null, value.ToUnixTimeSeconds()); }
-        }
-
-        private DateTimeOffset UnixTimeToDateTimeOffset(long seconds, long nanoseconds)
-        {
-            return DateTimeOffset.FromUnixTimeSeconds(seconds).AddTicks(nanoseconds / NanosecondsPerTick).ToLocalTime();
+            get => _fileStatus.GetLastWriteTime(FullPath);
+            set => _fileStatus.SetLastWriteTime(FullPath, value);
         }
 
-        private void SetAccessWriteTimes(long? accessTime, long? writeTime)
-        {
-            _fileStatusInitialized = -1; // force a refresh so that we have an up-to-date times for values not being overwritten
-            EnsureStatInitialized();
-            Interop.Sys.UTimBuf buf;
-            // we use utime() not utimensat() so we drop the subsecond part
-            buf.AcTime = accessTime ?? _fileStatus.ATime;
-            buf.ModTime = writeTime ?? _fileStatus.MTime;
-            bool isDirectory = this is DirectoryInfo;
-            Interop.CheckIo(Interop.Sys.UTime(FullPath, ref buf), FullPath, isDirectory);
-            _fileStatusInitialized = -1;
-        }
-
-        internal long LengthCore
-        {
-            get
-            {
-                EnsureStatInitialized();
-                return _fileStatus.Size;
-            }
-        }
-
-        public void Refresh()
-        {
-            // This should not throw, instead we store the result so that we can throw it
-            // when someone actually accesses a property.
+        internal long LengthCore => _fileStatus.GetLength(FullPath);
 
-            // Use lstat to get the details on the object, without following symlinks.
-            // If it is a symlink, then subsequently get details on the target of the symlink,
-            // storing those results separately.  We only report failure if the initial
-            // lstat fails, as a broken symlink should still report info on exists, attributes, etc.
-            _targetOfSymlinkIsDirectory = false;
-            string path = PathHelpers.TrimEndingDirectorySeparator(FullPath);
-            int result = Interop.Sys.LStat(path, out _fileStatus);
-            if (result < 0)
-            {
-                Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
+        public void Refresh() => _fileStatus.Refresh(FullPath);
 
-                // This should never set the error if the file can't be found.
-                // (see the Windows refresh passing returnErrorOnNotFound: false).
-                if (errorInfo.Error == Interop.Error.ENOENT
-                    || errorInfo.Error == Interop.Error.ENOTDIR)
-                {
-                    _fileStatusInitialized = 0;
-                    _exists = false;
-                }
-                else
-                {
-                    _fileStatusInitialized = errorInfo.RawErrno;
-                }
-                return;
-            }
-
-            _exists = true;
-
-            Interop.Sys.FileStatus targetStatus;
-            if ((_fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK &&
-                Interop.Sys.Stat(path, out targetStatus) >= 0)
-            {
-                _targetOfSymlinkIsDirectory = (targetStatus.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR;
-            }
-
-            _fileStatusInitialized = 0;
-        }
-
-        private void EnsureStatInitialized()
+        internal static void ThrowNotFound(string path)
         {
-            if (_fileStatusInitialized == -1)
-            {
-                Refresh();
-            }
+            // Windows distinguishes between whether the directory or the file isn't found,
+            // and throws a different exception in these cases.  We attempt to approximate that
+            // here; there is a race condition here, where something could change between
+            // when the error occurs and our checks, but it's the best we can do, and the
+            // worst case in such a race condition (which could occur if the file system is
+            // being manipulated concurrently with these checks) is that we throw a
+            // FileNotFoundException instead of DirectoryNotFoundException.
 
-            if (_fileStatusInitialized != 0)
-            {
-                int errno = _fileStatusInitialized;
-                _fileStatusInitialized = -1;
-                throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(errno), FullPath);
-            }
+            bool directoryError = !Directory.Exists(Path.GetDirectoryName(PathHelpers.TrimEndingDirectorySeparator(path)));
+            throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(Interop.Error.ENOENT), path, directoryError);
         }
     }
 }
diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/FileSystemInfo.Win32.cs b/src/libraries/System.IO.FileSystem/src/System/IO/FileSystemInfo.Win32.cs
deleted file mode 100644 (file)
index da72a66..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-// 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.
-
-namespace System.IO
-{
-    partial class FileSystemInfo
-    {
-        internal void Invalidate()
-        {
-            _dataInitialized = -1;
-        }
-    }
-}
index 41e0865..f45a713 100644 (file)
@@ -17,6 +17,15 @@ namespace System.IO
         // throw an appropriate error when attempting to access the cached info.
         private int _dataInitialized = -1;
 
+        protected FileSystemInfo()
+        {
+        }
+
+        internal void Invalidate()
+        {
+            _dataInitialized = -1;
+        }
+
         internal unsafe void Init(Interop.NtDll.FILE_FULL_DIR_INFORMATION* info)
         {
             _data.dwFileAttributes = (int)info->FileAttributes;
index 239377b..25eab4f 100644 (file)
@@ -14,10 +14,6 @@ namespace System.IO
 
         internal string _name;
 
-        protected FileSystemInfo()
-        {
-        }
-
         protected FileSystemInfo(SerializationInfo info, StreamingContext context)
         {
             throw new PlatformNotSupportedException();
index 7aa609b..224e2e3 100644 (file)
@@ -63,8 +63,6 @@ namespace System.IO.Tests.Enumeration
         }
     }
 
-    // Unix implementation not finished
-    [ActiveIssue(26715, TestPlatforms.AnyUnix)]
     public class SkipAttributeTests_Directory_GetFiles : SkipAttributeTests
     {
         protected override string[] GetPaths(string directory, EnumerationOptions options)
@@ -73,8 +71,6 @@ namespace System.IO.Tests.Enumeration
         }
     }
 
-    // Unix implementation not finished
-    [ActiveIssue(26715, TestPlatforms.AnyUnix)]
     public class SkipAttributeTests_DirectoryInfo_GetFiles : SkipAttributeTests
     {
         protected override string[] GetPaths(string directory, EnumerationOptions options)