diff options
author | Jeremy Kuhne <jeremy.kuhne@microsoft.com> | 2018-02-06 02:28:14 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-06 02:28:14 +0300 |
commit | 66ada723c1b0ae9062a0e7b0b28862014b985019 (patch) | |
tree | 213ff5181f3399b1204f95394120da9d7d3e7034 | |
parent | 4adbec49e0bed722d17b1c1f1ee4cbc2124f0937 (diff) |
File enumeration extensibility (#26806)
* Windows file enumeration extensibility
This is the Windows implementation of System.IO.Enumeration. It adds
new find options to existing APIs and a public extensibilty model
for richer, low allocation file system enumeration.
The Unix implementation is in progress.
* Initial naive Unix implementation of FileSystemEnumerator
* Move a netcoreapp test to the right group
* Fix WinRT build.
* Address feedback from @danmosemsft
* Remove inline attribute
* Remove IsNameDotOrDotDot property
* Address futher feedback.
* Remove unreferenced common code
36 files changed, 1636 insertions, 1277 deletions
diff --git a/src/Common/src/Interop/Windows/Interop.LongFileTime.cs b/src/Common/src/Interop/Windows/Interop.LongFileTime.cs index 411ae9e4c8..15b0767d24 100644 --- a/src/Common/src/Interop/Windows/Interop.LongFileTime.cs +++ b/src/Common/src/Interop/Windows/Interop.LongFileTime.cs @@ -23,6 +23,6 @@ internal partial class Interop internal long TicksSince1601; #pragma warning restore CS0649 - internal DateTime ToDateTimeUtc() => DateTime.FromFileTimeUtc(TicksSince1601); + internal DateTimeOffset ToDateTimeOffset() => DateTimeOffset.FromFileTime(TicksSince1601); } } diff --git a/src/Common/src/Interop/Windows/NtDll/Interop.FILE_FULL_DIR_INFORMATION.cs b/src/Common/src/Interop/Windows/NtDll/Interop.FILE_FULL_DIR_INFORMATION.cs index a1037696f6..5b3123bacf 100644 --- a/src/Common/src/Interop/Windows/NtDll/Interop.FILE_FULL_DIR_INFORMATION.cs +++ b/src/Common/src/Interop/Windows/NtDll/Interop.FILE_FULL_DIR_INFORMATION.cs @@ -62,6 +62,9 @@ internal partial class Interop /// </summary> public unsafe static FILE_FULL_DIR_INFORMATION* GetNextInfo(FILE_FULL_DIR_INFORMATION* info) { + if (info == null) + return null; + uint nextOffset = (*info).NextEntryOffset; if (nextOffset == 0) return null; diff --git a/src/System.IO.FileSystem/ref/System.IO.FileSystem.cs b/src/System.IO.FileSystem/ref/System.IO.FileSystem.cs index e4ba63826d..b373a812a9 100644 --- a/src/System.IO.FileSystem/ref/System.IO.FileSystem.cs +++ b/src/System.IO.FileSystem/ref/System.IO.FileSystem.cs @@ -20,12 +20,15 @@ namespace System.IO public static System.Collections.Generic.IEnumerable<string> EnumerateDirectories(string path) { throw null; } public static System.Collections.Generic.IEnumerable<string> EnumerateDirectories(string path, string searchPattern) { throw null; } public static System.Collections.Generic.IEnumerable<string> EnumerateDirectories(string path, string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public static System.Collections.Generic.IEnumerable<string> EnumerateDirectories(string path, string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public static System.Collections.Generic.IEnumerable<string> EnumerateFiles(string path) { throw null; } public static System.Collections.Generic.IEnumerable<string> EnumerateFiles(string path, string searchPattern) { throw null; } public static System.Collections.Generic.IEnumerable<string> EnumerateFiles(string path, string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public static System.Collections.Generic.IEnumerable<string> EnumerateFiles(string path, string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public static System.Collections.Generic.IEnumerable<string> EnumerateFileSystemEntries(string path) { throw null; } public static System.Collections.Generic.IEnumerable<string> EnumerateFileSystemEntries(string path, string searchPattern) { throw null; } public static System.Collections.Generic.IEnumerable<string> EnumerateFileSystemEntries(string path, string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public static System.Collections.Generic.IEnumerable<string> EnumerateFileSystemEntries(string path, string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public static bool Exists(string path) { throw null; } public static System.DateTime GetCreationTime(string path) { throw null; } public static System.DateTime GetCreationTimeUtc(string path) { throw null; } @@ -33,13 +36,16 @@ namespace System.IO public static string[] GetDirectories(string path) { throw null; } public static string[] GetDirectories(string path, string searchPattern) { throw null; } public static string[] GetDirectories(string path, string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public static string[] GetDirectories(string path, string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public static string GetDirectoryRoot(string path) { throw null; } public static string[] GetFiles(string path) { throw null; } public static string[] GetFiles(string path, string searchPattern) { throw null; } public static string[] GetFiles(string path, string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public static string[] GetFiles(string path, string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public static string[] GetFileSystemEntries(string path) { throw null; } public static string[] GetFileSystemEntries(string path, string searchPattern) { throw null; } public static string[] GetFileSystemEntries(string path, string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public static string[] GetFileSystemEntries(string path, string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public static System.DateTime GetLastAccessTime(string path) { throw null; } public static System.DateTime GetLastAccessTimeUtc(string path) { throw null; } public static System.DateTime GetLastWriteTime(string path) { throw null; } @@ -67,23 +73,29 @@ namespace System.IO public override void Delete() { } public void Delete(bool recursive) { } public System.Collections.Generic.IEnumerable<System.IO.DirectoryInfo> EnumerateDirectories() { throw null; } + public System.Collections.Generic.IEnumerable<System.IO.DirectoryInfo> EnumerateDirectories(string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } + public System.Collections.Generic.IEnumerable<System.IO.FileSystemInfo> EnumerateFileSystemInfos(string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public System.Collections.Generic.IEnumerable<System.IO.DirectoryInfo> EnumerateDirectories(string searchPattern) { throw null; } - public System.Collections.Generic.IEnumerable<System.IO.DirectoryInfo> EnumerateDirectories(string searchPattern, System.IO.SearchOption searchOption) { throw null; } public System.Collections.Generic.IEnumerable<System.IO.FileInfo> EnumerateFiles() { throw null; } public System.Collections.Generic.IEnumerable<System.IO.FileInfo> EnumerateFiles(string searchPattern) { throw null; } public System.Collections.Generic.IEnumerable<System.IO.FileInfo> EnumerateFiles(string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public System.Collections.Generic.IEnumerable<System.IO.FileInfo> EnumerateFiles(string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public System.Collections.Generic.IEnumerable<System.IO.FileSystemInfo> EnumerateFileSystemInfos() { throw null; } public System.Collections.Generic.IEnumerable<System.IO.FileSystemInfo> EnumerateFileSystemInfos(string searchPattern) { throw null; } public System.Collections.Generic.IEnumerable<System.IO.FileSystemInfo> EnumerateFileSystemInfos(string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public System.Collections.Generic.IEnumerable<System.IO.DirectoryInfo> EnumerateDirectories(string searchPattern, System.IO.SearchOption searchOption) { throw null; } public System.IO.DirectoryInfo[] GetDirectories() { throw null; } public System.IO.DirectoryInfo[] GetDirectories(string searchPattern) { throw null; } public System.IO.DirectoryInfo[] GetDirectories(string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public System.IO.DirectoryInfo[] GetDirectories(string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public System.IO.FileInfo[] GetFiles() { throw null; } public System.IO.FileInfo[] GetFiles(string searchPattern) { throw null; } public System.IO.FileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public System.IO.FileInfo[] GetFiles(string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public System.IO.FileSystemInfo[] GetFileSystemInfos() { throw null; } public System.IO.FileSystemInfo[] GetFileSystemInfos(string searchPattern) { throw null; } public System.IO.FileSystemInfo[] GetFileSystemInfos(string searchPattern, System.IO.SearchOption searchOption) { throw null; } + public System.IO.FileSystemInfo[] GetFileSystemInfos(string searchPattern, System.IO.Enumeration.EnumerationOptions enumerationOptions) { throw null; } public void MoveTo(string destDirName) { } public override string ToString() { throw null; } } @@ -212,3 +224,75 @@ namespace System.IO TopDirectoryOnly = 0, } } +namespace System.IO.Enumeration +{ + public enum MatchType + { + Simple, + Dos + } + public class EnumerationOptions + { + public EnumerationOptions() { } + + public bool RecurseSubdirectories { get { throw null; } set { } } + public bool IgnoreInaccessible { get { throw null; } set { } } + public int BufferSize { get { throw null; } set { } } + public FileAttributes AttributesToSkip { get { throw null; } set { } } + public MatchType MatchType { get { throw null; } set { } } + public bool ReturnSpecialDirectories { get { throw null; } set { } } + } + public ref struct FileSystemEntry + { + public ReadOnlySpan<char> Directory { get { throw null; } } + public string RootDirectory { get { throw null; } } + public string OriginalRootDirectory { get { throw null; } } + public ReadOnlySpan<char> FileName { get { throw null; } } + public FileAttributes Attributes { get { throw null; } } + public long Length { get { throw null; } } + public DateTimeOffset CreationTimeUtc { get { throw null; } } + public DateTimeOffset LastAccessTimeUtc { get { throw null; } } + public DateTimeOffset LastWriteTimeUtc { get { throw null; } } + public bool IsDirectory { get { throw null; } } + public FileSystemInfo ToFileSystemInfo() { throw null; } + public string ToSpecifiedFullPath() { throw null; } + public string ToFullPath() { throw null; } + } + public abstract class FileSystemEnumerator<TResult> : Runtime.ConstrainedExecution.CriticalFinalizerObject, Collections.Generic.IEnumerator<TResult> + { + public FileSystemEnumerator(string directory, EnumerationOptions options = null) { } + + protected virtual bool ShouldIncludeEntry(ref FileSystemEntry entry) { throw null; } + protected virtual bool ShouldRecurseIntoEntry(ref FileSystemEntry entry) { throw null; } + protected abstract TResult TransformEntry(ref FileSystemEntry entry); + protected virtual void OnDirectoryFinished(ReadOnlySpan<char> directory) { throw null; } + protected virtual bool ContinueOnError(int error) { throw null; } + + public TResult Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public bool MoveNext() { throw null; } + public void Reset() { throw null; } + public void Dispose() { throw null; } + protected virtual void Dispose(bool disposing) { throw null; } + } + public class FileSystemEnumerable<TResult> : Collections.Generic.IEnumerable<TResult> + { + public FileSystemEnumerable(string directory, FindTransform transform, EnumerationOptions options = null) { } + + public FindPredicate ShouldRecursePredicate { get { throw null; } set { } } + public FindPredicate ShouldIncludePredicate { get { throw null; } set { } } + + public Collections.Generic.IEnumerator<TResult> GetEnumerator() { throw null; } + Collections.IEnumerator Collections.IEnumerable.GetEnumerator() { throw null; } + + + public delegate bool FindPredicate(ref FileSystemEntry entry); + public delegate TResult FindTransform(ref FileSystemEntry entry); + } + public static class FileSystemName + { + public static string TranslateDosExpression(string expression) { throw null; } + public static bool MatchesDosExpression(ReadOnlySpan<char> expression, ReadOnlySpan<char> name, bool ignoreCase = true) { throw null; } + public static bool MatchesSimpleExpression(ReadOnlySpan<char> expression, ReadOnlySpan<char> name, bool ignoreCase = true) { throw null; } + } +} diff --git a/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj b/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj index f60c46292f..b738420619 100644 --- a/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj +++ b/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj @@ -19,28 +19,24 @@ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'uap-Windows_NT-Release|AnyCPU'" /> <ItemGroup> <Compile Include="System\IO\CharSpanExtensions.cs" /> - <Compile Include="System\IO\DosMatcher.cs" /> + <Compile Include="System\IO\Enumeration\FileSystemEnumerable.cs" /> + <Compile Include="System\IO\Enumeration\FileSystemEnumerableFactory.cs" /> + <Compile Include="System\IO\Enumeration\FileSystemEnumerator.cs" /> + <Compile Include="System\IO\Enumeration\FileSystemName.cs" /> + <Compile Include="System\IO\Enumeration\MatchType.cs" /> <Compile Include="System\IO\Error.cs" /> <Compile Include="System\IO\Directory.cs" /> <Compile Include="System\IO\DirectoryInfo.cs" /> <Compile Include="System\IO\File.cs" /> <Compile Include="System\IO\FileInfo.cs" /> <Compile Include="System\IO\FileSystemInfo.cs" /> + <Compile Include="System\IO\Enumeration\EnumerationOptions.cs" /> <Compile Include="System\IO\Iterator.cs" /> <Compile Include="System\IO\PathHelpers.cs" /> <Compile Include="System\IO\PathPair.cs" /> <Compile Include="System\IO\ReadLinesIterator.cs" /> <Compile Include="System\IO\SearchOption.cs" /> <Compile Include="System\IO\SearchTarget.cs" /> - <Compile Include="$(CommonPath)\System\Collections\Generic\ArrayBuilder.cs"> - <Link>Common\System\Collections\Generic\ArrayBuilder.cs</Link> - </Compile> - <Compile Include="$(CommonPath)\System\Collections\Generic\EnumerableHelpers.cs"> - <Link>Common\System\Collections\Generic\EnumerableHelpers.cs</Link> - </Compile> - <Compile Include="$(CommonPath)\System\Collections\Generic\LargeArrayBuilder.cs"> - <Link>Common\System\Collections\Generic\LargeArrayBuilder.cs</Link> - </Compile> <Compile Include="$(CommonPath)\System\IO\StringBuilderCache.cs"> <Link>Common\System\IO\StringBuilderCache.cs</Link> </Compile> @@ -62,19 +58,14 @@ </ItemGroup> <!-- Windows --> <ItemGroup Condition="'$(TargetsWindows)' == 'true'"> + <Compile Include="System\IO\Enumeration\FileSystemEnumerator.Windows.cs" /> <Compile Include="System\IO\CharSpanExtensions.Windows.cs" /> <Compile Include="System\IO\DisableMediaInsertionPrompt.cs" /> <Compile Include="System\IO\DirectoryInfo.Windows.cs" /> <Compile Include="System\IO\FileInfo.Windows.cs" /> - <Compile Include="System\IO\FindEnumerable.Windows.cs" /> - <Compile Include="System\IO\FindEnumerableFactory.cs" /> - <Compile Include="System\IO\FindPredicate.cs" /> - <Compile Include="System\IO\FindTransform.cs" /> - <Compile Include="System\IO\FindPredicates.cs" /> - <Compile Include="System\IO\FindTransforms.cs" /> <Compile Include="System\IO\FileSystemInfo.Windows.cs" /> <Compile Include="System\IO\PathHelpers.Windows.cs" /> - <Compile Include="System\IO\RawFindData.cs" /> + <Compile Include="System\IO\Enumeration\FileSystemEntry.Windows.cs" /> <Compile Include="System\IO\FileSystem.Windows.cs" /> <Compile Include="Microsoft\Win32\SafeHandles\SafeFindHandle.Windows.cs" /> <Compile Include="$(CommonPath)\Interop\Windows\Interop.Libraries.cs"> @@ -246,7 +237,7 @@ </ItemGroup> <!-- Windows : Win32 only --> <ItemGroup Condition="'$(TargetsWindows)' == 'true' and '$(UWPCompatible)' != 'true'"> - <Compile Include="System\IO\FindEnumerable.Win32.cs" /> + <Compile Include="System\IO\Enumeration\FileSystemEnumerator.Win32.cs" /> <Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CreateFile.cs"> <Link>Common\Interop\Windows\Interop.CreateFile.cs</Link> </Compile> @@ -274,7 +265,7 @@ </ItemGroup> <!-- Windows : UAP - Win32 + WinRT --> <ItemGroup Condition="'$(TargetsWindows)' == 'true' and '$(UWPCompatible)' == 'true'"> - <Compile Include="System\IO\FindEnumerable.WinRT.cs" /> + <Compile Include="System\IO\Enumeration\FileSystemEnumerator.WinRT.cs" /> <Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CreateFile2.cs"> <Link>Common\Interop\Windows\Interop.CreateFile2.cs</Link> </Compile> @@ -290,6 +281,8 @@ </ItemGroup> <!-- Unix --> <ItemGroup Condition="'$(TargetsUnix)' == 'true'"> + <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\FileSystemInfo.Unix.cs" /> <Compile Include="System\IO\PathHelpers.Unix.cs" /> @@ -405,6 +398,7 @@ <Reference Include="System.Collections" /> <Reference Include="System.Diagnostics.Debug" /> <Reference Include="System.Diagnostics.Tools" /> + <Reference Include="System.Linq" /> <Reference Include="System.Memory" /> <Reference Include="System.Resources.ResourceManager" /> <Reference Include="System.Runtime" /> @@ -419,4 +413,4 @@ <Reference Include="System.Threading" /> </ItemGroup> <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" /> -</Project> +</Project>
\ No newline at end of file diff --git a/src/System.IO.FileSystem/src/System/IO/Directory.cs b/src/System.IO.FileSystem/src/System/IO/Directory.cs index 5f4b32550e..1c7fc09638 100644 --- a/src/System.IO.FileSystem/src/System/IO/Directory.cs +++ b/src/System.IO.FileSystem/src/System/IO/Directory.cs @@ -3,7 +3,8 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Diagnostics; +using System.IO.Enumeration; +using System.Linq; using System.Security; namespace System.IO @@ -148,286 +149,94 @@ namespace System.IO return File.GetLastAccessTimeUtc(path); } - // Returns an array of filenames in the DirectoryInfo specified by path - public static string[] GetFiles(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); + public static string[] GetFiles(string path) => GetFiles(path, "*", enumerationOptions: EnumerationOptions.Compatible); - return InternalGetFiles(path, "*", SearchOption.TopDirectoryOnly); - } + public static string[] GetFiles(string path, string searchPattern) => GetFiles(path, searchPattern, enumerationOptions: EnumerationOptions.Compatible); - // Returns an array of Files in the current DirectoryInfo matching the - // given search pattern (i.e. "*.txt"). - public static string[] GetFiles(string path, string searchPattern) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - - return InternalGetFiles(path, searchPattern, SearchOption.TopDirectoryOnly); - } - - // Returns an array of Files in the current DirectoryInfo matching the - // given search pattern (i.e. "*.txt") and search option public static string[] GetFiles(string path, string searchPattern, SearchOption searchOption) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); + => GetFiles(path, searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - return InternalGetFiles(path, searchPattern, searchOption); - } + public static string[] GetFiles(string path, string searchPattern, EnumerationOptions enumerationOptions) + => InternalEnumeratePaths(path, searchPattern, SearchTarget.Files, enumerationOptions).ToArray(); - // Returns an array of Files in the current DirectoryInfo matching the - // given search pattern (i.e. "*.txt") and search option - private static string[] InternalGetFiles(string path, string searchPattern, SearchOption searchOption) - { - Debug.Assert(path != null); - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); + public static string[] GetDirectories(string path) => GetDirectories(path, "*", enumerationOptions: EnumerationOptions.Compatible); - return InternalGetFileDirectoryNames(path, path, searchPattern, true, false, searchOption); - } - - // Returns an array of Directories in the current directory. - public static string[] GetDirectories(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - - return InternalGetDirectories(path, "*", SearchOption.TopDirectoryOnly); - } - - // Returns an array of Directories in the current DirectoryInfo matching the - // given search criteria (i.e. "*.txt"). - public static string[] GetDirectories(string path, string searchPattern) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - - return InternalGetDirectories(path, searchPattern, SearchOption.TopDirectoryOnly); - } + public static string[] GetDirectories(string path, string searchPattern) => GetDirectories(path, searchPattern, enumerationOptions: EnumerationOptions.Compatible); - // Returns an array of Directories in the current DirectoryInfo matching the - // given search criteria (i.e. "*.txt"). public static string[] GetDirectories(string path, string searchPattern, SearchOption searchOption) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); + => GetDirectories(path, searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - return InternalGetDirectories(path, searchPattern, searchOption); - } + public static string[] GetDirectories(string path, string searchPattern, EnumerationOptions enumerationOptions) + => InternalEnumeratePaths(path, searchPattern, SearchTarget.Directories, enumerationOptions).ToArray(); - // Returns an array of Directories in the current DirectoryInfo matching the - // given search criteria (i.e. "*.txt"). - private static string[] InternalGetDirectories(string path, string searchPattern, SearchOption searchOption) - { - Debug.Assert(path != null); - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); + public static string[] GetFileSystemEntries(string path) => GetFileSystemEntries(path, "*", enumerationOptions: EnumerationOptions.Compatible); - return InternalGetFileDirectoryNames(path, path, searchPattern, false, true, searchOption); - } + public static string[] GetFileSystemEntries(string path, string searchPattern) => GetFileSystemEntries(path, searchPattern, enumerationOptions: EnumerationOptions.Compatible); - // Returns an array of strongly typed FileSystemInfo entries in the path - public static string[] GetFileSystemEntries(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - - return InternalGetFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly); - } - - // Returns an array of strongly typed FileSystemInfo entries in the path with the - // given search criteria (i.e. "*.txt"). We disallow .. as a part of the search criteria - public static string[] GetFileSystemEntries(string path, string searchPattern) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); + public static string[] GetFileSystemEntries(string path, string searchPattern, SearchOption searchOption) + => GetFileSystemEntries(path, searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - return InternalGetFileSystemEntries(path, searchPattern, SearchOption.TopDirectoryOnly); - } + public static string[] GetFileSystemEntries(string path, string searchPattern, EnumerationOptions enumerationOptions) + => InternalEnumeratePaths(path, searchPattern, SearchTarget.Both, enumerationOptions).ToArray(); - // Returns an array of strongly typed FileSystemInfo entries in the path with the - // given search criteria (i.e. "*.txt"). We disallow .. as a part of the search criteria - public static string[] GetFileSystemEntries(string path, string searchPattern, SearchOption searchOption) + internal static IEnumerable<string> InternalEnumeratePaths( + string path, + string searchPattern, + SearchTarget searchTarget, + EnumerationOptions options) { if (path == null) throw new ArgumentNullException(nameof(path)); if (searchPattern == null) throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); - - return InternalGetFileSystemEntries(path, searchPattern, searchOption); - } - - private static string[] InternalGetFileSystemEntries(string path, string searchPattern, SearchOption searchOption) - { - Debug.Assert(path != null); - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); - - return InternalGetFileDirectoryNames(path, path, searchPattern, true, true, searchOption); - } - - // Returns fully qualified user path of dirs/files that matches the search parameters. - // For recursive search this method will search through all the sub dirs and execute - // the given search criteria against every dir. - // For all the dirs/files returned, it will then demand path discovery permission for - // their parent folders (it will avoid duplicate permission checks) - internal static string[] InternalGetFileDirectoryNames(string path, string userPathOriginal, string searchPattern, bool includeFiles, bool includeDirs, SearchOption searchOption) - { - Debug.Assert(path != null); - Debug.Assert(userPathOriginal != null); - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); - - IEnumerable<string> enumerable = FileSystem.EnumeratePaths(path, searchPattern, searchOption, - (includeFiles ? SearchTarget.Files : 0) | (includeDirs ? SearchTarget.Directories : 0)); - return EnumerableHelpers.ToArray(enumerable); - } - public static IEnumerable<string> EnumerateDirectories(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); + FileSystemEnumerableFactory.NormalizeInputs(ref path, ref searchPattern, options); - return InternalEnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly); + switch (searchTarget) + { + case SearchTarget.Files: + return FileSystemEnumerableFactory.UserFiles(path, searchPattern, options); + case SearchTarget.Directories: + return FileSystemEnumerableFactory.UserDirectories(path, searchPattern, options); + case SearchTarget.Both: + return FileSystemEnumerableFactory.UserEntries(path, searchPattern, options); + default: + throw new ArgumentOutOfRangeException(nameof(searchTarget)); + } } - public static IEnumerable<string> EnumerateDirectories(string path, string searchPattern) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); + public static IEnumerable<string> EnumerateDirectories(string path) => EnumerateDirectories(path, "*", enumerationOptions: EnumerationOptions.Compatible); - return InternalEnumerateDirectories(path, searchPattern, SearchOption.TopDirectoryOnly); - } + public static IEnumerable<string> EnumerateDirectories(string path, string searchPattern) => EnumerateDirectories(path, searchPattern, enumerationOptions: EnumerationOptions.Compatible); public static IEnumerable<string> EnumerateDirectories(string path, string searchPattern, SearchOption searchOption) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); - - return InternalEnumerateDirectories(path, searchPattern, searchOption); - } - - private static IEnumerable<string> InternalEnumerateDirectories(string path, string searchPattern, SearchOption searchOption) - { - Debug.Assert(path != null); - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); + => EnumerateDirectories(path, searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - return EnumerateFileSystemNames(path, searchPattern, searchOption, false, true); - } + public static IEnumerable<string> EnumerateDirectories(string path, string searchPattern, EnumerationOptions enumerationOptions) + => InternalEnumeratePaths(path, searchPattern, SearchTarget.Directories, enumerationOptions); - public static IEnumerable<string> EnumerateFiles(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - - return InternalEnumerateFiles(path, "*", SearchOption.TopDirectoryOnly); - } + public static IEnumerable<string> EnumerateFiles(string path) => EnumerateFiles(path, "*", enumerationOptions: EnumerationOptions.Compatible); public static IEnumerable<string> EnumerateFiles(string path, string searchPattern) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - - return InternalEnumerateFiles(path, searchPattern, SearchOption.TopDirectoryOnly); - } + => EnumerateFiles(path, searchPattern, enumerationOptions: EnumerationOptions.Compatible); public static IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); + => EnumerateFiles(path, searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - return InternalEnumerateFiles(path, searchPattern, searchOption); - } - - private static IEnumerable<string> InternalEnumerateFiles(string path, string searchPattern, SearchOption searchOption) - { - Debug.Assert(path != null); - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); - - return EnumerateFileSystemNames(path, searchPattern, searchOption, true, false); - } + public static IEnumerable<string> EnumerateFiles(string path, string searchPattern, EnumerationOptions enumerationOptions) + => InternalEnumeratePaths(path, searchPattern, SearchTarget.Files, enumerationOptions); public static IEnumerable<string> EnumerateFileSystemEntries(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - - return InternalEnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly); - } + => EnumerateFileSystemEntries(path, "*", enumerationOptions: EnumerationOptions.Compatible); public static IEnumerable<string> EnumerateFileSystemEntries(string path, string searchPattern) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - - return InternalEnumerateFileSystemEntries(path, searchPattern, SearchOption.TopDirectoryOnly); - } + => EnumerateFileSystemEntries(path, searchPattern, enumerationOptions: EnumerationOptions.Compatible); public static IEnumerable<string> EnumerateFileSystemEntries(string path, string searchPattern, SearchOption searchOption) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); - - return InternalEnumerateFileSystemEntries(path, searchPattern, searchOption); - } - - private static IEnumerable<string> InternalEnumerateFileSystemEntries(string path, string searchPattern, SearchOption searchOption) - { - Debug.Assert(path != null); - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); - - return EnumerateFileSystemNames(path, searchPattern, searchOption, true, true); - } - - private static IEnumerable<string> EnumerateFileSystemNames(string path, string searchPattern, SearchOption searchOption, - bool includeFiles, bool includeDirs) - { - Debug.Assert(path != null); - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); + => EnumerateFileSystemEntries(path, searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - return FileSystem.EnumeratePaths(path, searchPattern, searchOption, - (includeFiles ? SearchTarget.Files : 0) | (includeDirs ? SearchTarget.Directories : 0)); - } + public static IEnumerable<string> EnumerateFileSystemEntries(string path, string searchPattern, EnumerationOptions enumerationOptions) + => InternalEnumeratePaths(path, searchPattern, SearchTarget.Both, enumerationOptions); public static string GetDirectoryRoot(string path) { @@ -446,13 +255,6 @@ namespace System.IO return path.Substring(0, PathInternal.GetRootLength(path)); } - /*===============================CurrentDirectory=============================== - **Action: Provides a getter and setter for the current directory. The original - ** current DirectoryInfo is the one from which the process was started. - **Returns: The current DirectoryInfo (from the getter). Void from the setter. - **Arguments: The current DirectoryInfo to which to switch to the setter. - **Exceptions: - ==============================================================================*/ public static string GetCurrentDirectory() { return FileSystem.GetCurrentDirectory(); diff --git a/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.Windows.cs b/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.Windows.cs index 1c1ebd0197..84e26c6820 100644 --- a/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.Windows.cs +++ b/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.Windows.cs @@ -2,17 +2,17 @@ // 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.Diagnostics; +using System.IO.Enumeration; namespace System.IO { partial class DirectoryInfo { - internal unsafe DirectoryInfo(string fullPath, string fileName, ref RawFindData findData) - : this(fullPath, fileName: fileName, isNormalized: true) + internal static unsafe DirectoryInfo Create(string fullPath, ref FileSystemEntry findData) { - Debug.Assert(fileName.Equals(Path.GetFileName(fullPath))); - Init(findData._info); + DirectoryInfo info = new DirectoryInfo(fullPath, fileName: findData.FileName.GetStringFromFixedBuffer(), isNormalized: true); + info.Init(findData._info); + return info; } } } diff --git a/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs b/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs index a23d9d7b45..1970b8939e 100644 --- a/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs +++ b/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Diagnostics; +using System.IO.Enumeration; +using System.Linq; namespace System.IO { @@ -63,7 +65,6 @@ namespace System.IO } } - public DirectoryInfo CreateSubdirectory(string path) { if (path == null) @@ -117,218 +118,104 @@ namespace System.IO } } - // Returns an array of Files in the current DirectoryInfo matching the - // given search criteria (i.e. "*.txt"). - public FileInfo[] GetFiles(string searchPattern) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - - return InternalGetFiles(searchPattern, SearchOption.TopDirectoryOnly); - } - - // Returns an array of Files in the current DirectoryInfo matching the - // given search criteria (i.e. "*.txt"). - public FileInfo[] GetFiles(string searchPattern, SearchOption searchOption) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); - - return InternalGetFiles(searchPattern, searchOption); - } + // Returns an array of Files in the DirectoryInfo specified by path + public FileInfo[] GetFiles() => GetFiles("*", enumerationOptions: EnumerationOptions.Compatible); // Returns an array of Files in the current DirectoryInfo matching the // given search criteria (i.e. "*.txt"). - private FileInfo[] InternalGetFiles(string searchPattern, SearchOption searchOption) - { - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); + public FileInfo[] GetFiles(string searchPattern) => GetFiles(searchPattern, enumerationOptions: EnumerationOptions.Compatible); - IEnumerable<FileInfo> enumerable = (IEnumerable<FileInfo>)FileSystem.EnumerateFileSystemInfos(FullPath, searchPattern, searchOption, SearchTarget.Files); - return EnumerableHelpers.ToArray(enumerable); - } + public FileInfo[] GetFiles(string searchPattern, SearchOption searchOption) + => GetFiles(searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - // Returns an array of Files in the DirectoryInfo specified by path - public FileInfo[] GetFiles() - { - return InternalGetFiles("*", SearchOption.TopDirectoryOnly); - } + public FileInfo[] GetFiles(string searchPattern, EnumerationOptions enumerationOptions) + => ((IEnumerable<FileInfo>)InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Files, enumerationOptions)).ToArray(); - // Returns an array of Directories in the current directory. - public DirectoryInfo[] GetDirectories() - { - return InternalGetDirectories("*", SearchOption.TopDirectoryOnly); - } + // Returns an array of strongly typed FileSystemInfo entries which will contain a listing + // of all the files and directories. + public FileSystemInfo[] GetFileSystemInfos() => GetFileSystemInfos("*", enumerationOptions: EnumerationOptions.Compatible); // Returns an array of strongly typed FileSystemInfo entries in the path with the // given search criteria (i.e. "*.txt"). public FileSystemInfo[] GetFileSystemInfos(string searchPattern) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); + => GetFileSystemInfos(searchPattern, enumerationOptions: EnumerationOptions.Compatible); - return InternalGetFileSystemInfos(searchPattern, SearchOption.TopDirectoryOnly); - } - - // Returns an array of strongly typed FileSystemInfo entries in the path with the - // given search criteria (i.e. "*.txt"). public FileSystemInfo[] GetFileSystemInfos(string searchPattern, SearchOption searchOption) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); - - return InternalGetFileSystemInfos(searchPattern, searchOption); - } - - // Returns an array of strongly typed FileSystemInfo entries in the path with the - // given search criteria (i.e. "*.txt"). - private FileSystemInfo[] InternalGetFileSystemInfos(string searchPattern, SearchOption searchOption) - { - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); + => GetFileSystemInfos(searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - IEnumerable<FileSystemInfo> enumerable = FileSystem.EnumerateFileSystemInfos(FullPath, searchPattern, searchOption, SearchTarget.Both); - return EnumerableHelpers.ToArray(enumerable); - } + public FileSystemInfo[] GetFileSystemInfos(string searchPattern, EnumerationOptions enumerationOptions) + => InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Both, enumerationOptions).ToArray(); - // Returns an array of strongly typed FileSystemInfo entries which will contain a listing - // of all the files and directories. - public FileSystemInfo[] GetFileSystemInfos() - { - return InternalGetFileSystemInfos("*", SearchOption.TopDirectoryOnly); - } + // Returns an array of Directories in the current directory. + public DirectoryInfo[] GetDirectories() => GetDirectories("*", enumerationOptions: EnumerationOptions.Compatible); // Returns an array of Directories in the current DirectoryInfo matching the - // given search criteria (i.e. "System*" could match the System & System32 - // directories). - public DirectoryInfo[] GetDirectories(string searchPattern) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - - return InternalGetDirectories(searchPattern, SearchOption.TopDirectoryOnly); - } + // given search criteria (i.e. "System*" could match the System & System32 directories). + public DirectoryInfo[] GetDirectories(string searchPattern) => GetDirectories(searchPattern, enumerationOptions: EnumerationOptions.Compatible); - // Returns an array of Directories in the current DirectoryInfo matching the - // given search criteria (i.e. "System*" could match the System & System32 - // directories). public DirectoryInfo[] GetDirectories(string searchPattern, SearchOption searchOption) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); - - return InternalGetDirectories(searchPattern, searchOption); - } - - // Returns an array of Directories in the current DirectoryInfo matching the - // given search criteria (i.e. "System*" could match the System & System32 - // directories). - private DirectoryInfo[] InternalGetDirectories(string searchPattern, SearchOption searchOption) - { - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); + => GetDirectories(searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - IEnumerable<DirectoryInfo> enumerable = (IEnumerable<DirectoryInfo>)FileSystem.EnumerateFileSystemInfos(FullPath, searchPattern, searchOption, SearchTarget.Directories); - return EnumerableHelpers.ToArray(enumerable); - } + public DirectoryInfo[] GetDirectories(string searchPattern, EnumerationOptions enumerationOptions) + => ((IEnumerable<DirectoryInfo>)InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Directories, enumerationOptions)).ToArray(); public IEnumerable<DirectoryInfo> EnumerateDirectories() - { - return InternalEnumerateDirectories("*", SearchOption.TopDirectoryOnly); - } + => EnumerateDirectories("*", enumerationOptions: EnumerationOptions.Compatible); public IEnumerable<DirectoryInfo> EnumerateDirectories(string searchPattern) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - - return InternalEnumerateDirectories(searchPattern, SearchOption.TopDirectoryOnly); - } + => EnumerateDirectories(searchPattern, enumerationOptions: EnumerationOptions.Compatible); public IEnumerable<DirectoryInfo> EnumerateDirectories(string searchPattern, SearchOption searchOption) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); - - return InternalEnumerateDirectories(searchPattern, searchOption); - } - - private IEnumerable<DirectoryInfo> InternalEnumerateDirectories(string searchPattern, SearchOption searchOption) - { - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); + => EnumerateDirectories(searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - return (IEnumerable<DirectoryInfo>)FileSystem.EnumerateFileSystemInfos(FullPath, searchPattern, searchOption, SearchTarget.Directories); - } + public IEnumerable<DirectoryInfo> EnumerateDirectories(string searchPattern, EnumerationOptions enumerationOptions) + => (IEnumerable<DirectoryInfo>)InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Directories, enumerationOptions); public IEnumerable<FileInfo> EnumerateFiles() - { - return InternalEnumerateFiles("*", SearchOption.TopDirectoryOnly); - } + => EnumerateFiles("*", enumerationOptions: EnumerationOptions.Compatible); - public IEnumerable<FileInfo> EnumerateFiles(string searchPattern) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - - return InternalEnumerateFiles(searchPattern, SearchOption.TopDirectoryOnly); - } + public IEnumerable<FileInfo> EnumerateFiles(string searchPattern) => EnumerateFiles(searchPattern, enumerationOptions: EnumerationOptions.Compatible); public IEnumerable<FileInfo> EnumerateFiles(string searchPattern, SearchOption searchOption) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); + => EnumerateFiles(searchPattern, EnumerationOptions.FromSearchOption(searchOption)); - return InternalEnumerateFiles(searchPattern, searchOption); - } - - private IEnumerable<FileInfo> InternalEnumerateFiles(string searchPattern, SearchOption searchOption) - { - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); + public IEnumerable<FileInfo> EnumerateFiles(string searchPattern, EnumerationOptions enumerationOptions) + => (IEnumerable<FileInfo>)InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Files, enumerationOptions); - return (IEnumerable<FileInfo>)FileSystem.EnumerateFileSystemInfos(FullPath, searchPattern, searchOption, SearchTarget.Files); - } - - public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos() - { - return InternalEnumerateFileSystemInfos("*", SearchOption.TopDirectoryOnly); - } + public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos() => EnumerateFileSystemInfos("*", enumerationOptions: EnumerationOptions.Compatible); public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string searchPattern) - { - if (searchPattern == null) - throw new ArgumentNullException(nameof(searchPattern)); - - return InternalEnumerateFileSystemInfos(searchPattern, SearchOption.TopDirectoryOnly); - } + => EnumerateFileSystemInfos(searchPattern, enumerationOptions: EnumerationOptions.Compatible); public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string searchPattern, SearchOption searchOption) + => EnumerateFileSystemInfos(searchPattern, EnumerationOptions.FromSearchOption(searchOption)); + + public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string searchPattern, EnumerationOptions enumerationOptions) + => InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Both, enumerationOptions); + + internal static IEnumerable<FileSystemInfo> InternalEnumerateInfos( + string path, + string searchPattern, + SearchTarget searchTarget, + EnumerationOptions options) { + Debug.Assert(path != null); if (searchPattern == null) throw new ArgumentNullException(nameof(searchPattern)); - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); - - return InternalEnumerateFileSystemInfos(searchPattern, searchOption); - } - private IEnumerable<FileSystemInfo> InternalEnumerateFileSystemInfos(string searchPattern, SearchOption searchOption) - { - Debug.Assert(searchPattern != null); - Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly); + FileSystemEnumerableFactory.NormalizeInputs(ref path, ref searchPattern, options); - return FileSystem.EnumerateFileSystemInfos(FullPath, searchPattern, searchOption, SearchTarget.Both); + switch (searchTarget) + { + case SearchTarget.Directories: + return FileSystemEnumerableFactory.DirectoryInfos(path, searchPattern, options); + case SearchTarget.Files: + return FileSystemEnumerableFactory.FileInfos(path, searchPattern, options); + case SearchTarget.Both: + return FileSystemEnumerableFactory.FileSystemInfos(path, searchPattern, options); + default: + throw new ArgumentException(SR.ArgumentOutOfRange_Enum, nameof(searchTarget)); + } } // Returns the root portion of the given path. The resulting string diff --git a/src/System.IO.FileSystem/src/System/IO/Enumeration/EnumerationOptions.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/EnumerationOptions.cs new file mode 100644 index 0000000000..164cf3ed6e --- /dev/null +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/EnumerationOptions.cs @@ -0,0 +1,84 @@ +// 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.Enumeration +{ + public class EnumerationOptions + { + /// <summary> + /// For internal use. These are the options we want to use if calling the existing Directory/File APIs where you don't + /// explicitly specify EnumerationOptions. + /// </summary> + internal static EnumerationOptions Compatible { get; } = new EnumerationOptions { MatchType = MatchType.Dos }; + + private static EnumerationOptions CompatibleRecursive { get; } = new EnumerationOptions { RecurseSubdirectories = true, MatchType = MatchType.Dos }; + + /// <summary> + /// Internal singleton for default options. + /// </summary> + internal static EnumerationOptions Default { get; } = new EnumerationOptions(); + + /// <summary> + /// Default constructor. Constructs the options class with recommended default options. + /// </summary> + public EnumerationOptions() + { + } + + /// <summary> + /// Converts SearchOptions to FindOptions. Throws if undefined SearchOption. + /// </summary> + internal static EnumerationOptions FromSearchOption(SearchOption searchOption) + { + if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) + throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum); + + return searchOption == SearchOption.AllDirectories ? CompatibleRecursive : Compatible; + } + + /// <summary> + /// Should we recurse into subdirectories while enumerating? + /// </summary> + public bool RecurseSubdirectories { get; set; } + + /// <summary> + /// Skip files/directories when access is denied (e.g. AccessDeniedException/SecurityException) + /// </summary> + public bool IgnoreInaccessible { get; set; } + + /// <summary> + /// Suggested buffer size, in bytes. + /// </summary> + /// <remarks> + /// Not all platforms use user allocated buffers, and some require either fixed buffers or a + /// buffer that has enough space to return a full result. One scenario where this option is + /// useful is with remote share enumeration on Windows. Having a large buffer may result in + /// better performance as more results can be batched over the wire (e.g. over a network + /// share). A "large" buffer, for example, would be 16K. Typical is 4K. + /// + /// We will not use the suggested buffer size if it has no meaning for the native APIs on the + /// current platform or if it would be too small for getting at least a single result. + /// </remarks> + public int BufferSize { get; set; } + + /// <summary> + /// Skip entries with the given attributes. + /// </summary> + public FileAttributes AttributesToSkip { get; set; } + + /// <summary> + /// For APIs that allow specifying a match expression this will allow you to specify how + /// to interpret the match expression. + /// </summary> + /// <remarks> + /// The default is simple matching where '*' is always 0 or more characters and '?' is a single character. + /// </remarks> + public MatchType MatchType { get; set; } + + /// <summary> + /// Set to true to return "." and ".." directory entries. + /// </summary> + public bool ReturnSpecialDirectories { get; set; } + } +} 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 new file mode 100644 index 0000000000..a4645cdf80 --- /dev/null +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Unix.cs @@ -0,0 +1,86 @@ +// 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.Enumeration +{ + /// <summary> + /// Lower level view of FileSystemInfo used for processing and filtering find results. + /// </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( + ref FileSystemEntry entry, + Interop.Sys.DirectoryEntry directoryEntry, + bool isDirectory, + ReadOnlySpan<char> directory, + string rootDirectory, + string originalRootDirectory) + { + entry._directoryEntry = directoryEntry; + entry._isDirectory = isDirectory; + entry.Directory = directory; + entry.RootDirectory = rootDirectory; + entry.OriginalRootDirectory = originalRootDirectory; + } + + internal Interop.Sys.DirectoryEntry _directoryEntry; + private FileSystemInfo _info; + private bool _isDirectory; + + private FileSystemInfo Info + { + get + { + if (_info == null) + { + 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(); + } + return _info; + } + } + + /// <summary> + /// The full path of the directory this entry resides in. + /// </summary> + public ReadOnlySpan<char> Directory { get; private set; } + + /// <summary> + /// The full path of the root directory used for the enumeration. + /// </summary> + public string RootDirectory { get; private set; } + + /// <summary> + /// The root directory for the enumeration as specified in the constructor. + /// </summary> + 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; + + /// <summary> + /// Returns the full path for find results, based on the initially provided path. + /// </summary> + public string ToSpecifiedFullPath() => + PathHelpers.CombineNoChecks(OriginalRootDirectory, Directory.Slice(RootDirectory.Length), FileName); + + /// <summary> + /// Returns the full path of the find result. + /// </summary> + public string ToFullPath() => + PathHelpers.CombineNoChecks(Directory, FileName); + } +} diff --git a/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Windows.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Windows.cs new file mode 100644 index 0000000000..48bf820148 --- /dev/null +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEntry.Windows.cs @@ -0,0 +1,91 @@ +// 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.Enumeration +{ + /// <summary> + /// Lower level view of FileSystemInfo used for processing and filtering find results. + /// </summary> + public unsafe ref struct FileSystemEntry + { + internal static void Initialize( + ref FileSystemEntry entry, + Interop.NtDll.FILE_FULL_DIR_INFORMATION* info, + ReadOnlySpan<char> directory, + string rootDirectory, + string originalRootDirectory) + { + entry._info = info; + entry.Directory = directory; + entry.RootDirectory = rootDirectory; + entry.OriginalRootDirectory = originalRootDirectory; + } + + internal unsafe Interop.NtDll.FILE_FULL_DIR_INFORMATION* _info; + + /// <summary> + /// The full path of the directory this entry resides in. + /// </summary> + public ReadOnlySpan<char> Directory { get; private set; } + + /// <summary> + /// The full path of the root directory used for the enumeration. + /// </summary> + public string RootDirectory { get; private set; } + + /// <summary> + /// The root directory for the enumeration as specified in the constructor. + /// </summary> + public string OriginalRootDirectory { get; private set; } + + /// <summary> + /// The file name for this entry. + /// </summary> + public ReadOnlySpan<char> FileName => _info->FileName; + + /// <summary> + /// The attributes for this entry. + /// </summary> + public FileAttributes Attributes => _info->FileAttributes; + + /// <summary> + /// The length of file in bytes. + /// </summary> + public long Length => _info->EndOfFile; + + /// <summary> + /// The creation time for the entry or the oldest available time stamp if the + /// operating system does not support creation time stamps. + /// </summary> + public DateTimeOffset CreationTimeUtc => _info->CreationTime.ToDateTimeOffset(); + public DateTimeOffset LastAccessTimeUtc => _info->LastAccessTime.ToDateTimeOffset(); + public DateTimeOffset LastWriteTimeUtc => _info->LastWriteTime.ToDateTimeOffset(); + + /// <summary> + /// Returns true if this entry is a directory. + /// </summary> + public bool IsDirectory => (Attributes & FileAttributes.Directory) != 0; + + public FileSystemInfo ToFileSystemInfo() + { + string fullPath = PathHelpers.CombineNoChecks(Directory, FileName); + + return IsDirectory + ? DirectoryInfo.Create(fullPath, ref this) + : (FileSystemInfo)FileInfo.Create(fullPath, ref this); + } + + /// <summary> + /// Returns the full path for find results, based on the initially provided path. + /// </summary> + public string ToSpecifiedFullPath() => + PathHelpers.CombineNoChecks(OriginalRootDirectory, Directory.Slice(RootDirectory.Length), FileName); + + /// <summary> + /// Returns the full path of the find result. + /// </summary> + public string ToFullPath() => + PathHelpers.CombineNoChecks(Directory, FileName); + } +} diff --git a/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerable.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerable.cs new file mode 100644 index 0000000000..b05b1375d6 --- /dev/null +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerable.cs @@ -0,0 +1,66 @@ +// 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.Collections; +using System.Collections.Generic; +using System.Threading; + +namespace System.IO.Enumeration +{ + public class FileSystemEnumerable<TResult> : IEnumerable<TResult> + { + private DelegateEnumerator _enumerator; + private readonly FindTransform _transform; + private readonly EnumerationOptions _options; + private readonly string _directory; + + public FileSystemEnumerable(string directory, FindTransform transform, EnumerationOptions options = null) + { + _directory = directory ?? throw new ArgumentNullException(nameof(directory)); + _transform = transform ?? throw new ArgumentNullException(nameof(transform)); + _options = options ?? EnumerationOptions.Default; + + // We need to create the enumerator up front to ensure that we throw I/O exceptions for + // the root directory on creation of the enumerable. + _enumerator = new DelegateEnumerator(this); + } + + public FindPredicate ShouldIncludePredicate { get; set; } + public FindPredicate ShouldRecursePredicate { get; set; } + + public IEnumerator<TResult> GetEnumerator() + { + return Interlocked.Exchange(ref _enumerator, null) ?? new DelegateEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// <summary> + /// Delegate for filtering out find results. + /// </summary> + public delegate bool FindPredicate(ref FileSystemEntry entry); + + /// <summary> + /// Delegate for transforming raw find data into a result. + /// </summary> + public delegate TResult FindTransform(ref FileSystemEntry entry); + + private sealed class DelegateEnumerator : FileSystemEnumerator<TResult> + { + private readonly FileSystemEnumerable<TResult> _enumerable; + + public DelegateEnumerator(FileSystemEnumerable<TResult> enumerable) + : base(enumerable._directory, enumerable._options) + { + _enumerable = enumerable; + } + + protected override TResult TransformEntry(ref FileSystemEntry entry) => _enumerable._transform(ref entry); + protected override bool ShouldRecurseIntoEntry(ref FileSystemEntry entry) + => _enumerable.ShouldRecursePredicate?.Invoke(ref entry) ?? true; + protected override bool ShouldIncludeEntry(ref FileSystemEntry entry) + => _enumerable.ShouldIncludePredicate?.Invoke(ref entry) ?? true; + } + } +} diff --git a/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerableFactory.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerableFactory.cs new file mode 100644 index 0000000000..a342528545 --- /dev/null +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerableFactory.cs @@ -0,0 +1,170 @@ +// 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 ref the project root for more information. + +using System.Collections.Generic; + +namespace System.IO.Enumeration +{ + internal static class FileSystemEnumerableFactory + { + // These all have special meaning in DOS name matching. '\' is the escaping character (which conveniently + // is the directory separator and cannot be part of any path segment in Windows). The other three are the + // special case wildcards that we'll convert some * and ? into. They're also valid as filenames on Unix, + // which is not true in Windows and as such we'll escape any that occur on the input string. + private readonly static char[] s_unixEscapeChars = { '\\', '"', '<', '>' }; + + internal static void NormalizeInputs(ref string directory, ref string expression, EnumerationOptions options) + { + if (Path.IsPathRooted(expression)) + throw new ArgumentException(SR.Arg_Path2IsRooted, nameof(expression)); + + // We always allowed breaking the passed ref directory and filter to be separated + // any way the user wanted. Looking for "C:\foo\*.cs" could be passed as "C:\" and + // "foo\*.cs" or "C:\foo" and "*.cs", for example. As such we need to combine and + // split the inputs if the expression contains a directory separator. + // + // We also allowed for expression to be "foo\" which would translate to "foo\*". + + ReadOnlySpan<char> directoryName = PathHelpers.GetDirectoryNameNoChecks(expression.AsReadOnlySpan()); + + if (directoryName.Length != 0) + { + // Need to fix up the input paths + directory = PathHelpers.CombineNoChecks(directory, directoryName); + expression = expression.Substring(directoryName.Length + 1); + } + + switch (options.MatchType) + { + case MatchType.Dos: + if (string.IsNullOrEmpty(expression) || expression == "." || expression == "*.*") + { + // Historically we always treated "." as "*" + expression = "*"; + } + else + { + if (Path.DirectorySeparatorChar != '\\' && expression.IndexOfAny(s_unixEscapeChars) != -1) + { + // Backslash isn't the default separator, need to escape (e.g. Unix) + expression = expression.Replace("\\", "\\\\"); + + // Also need to escape the other special wild characters ('"', '<', and '>') + expression = expression.Replace("\"", "\\\""); + expression = expression.Replace(">", "\\>"); + expression = expression.Replace("<", "\\<"); + } + + // Need to convert the expression to match Win32 behavior + expression = FileSystemName.TranslateDosExpression(expression); + } + break; + case MatchType.Simple: + break; + default: + throw new ArgumentOutOfRangeException(nameof(options)); + } + } + + private static bool MatchesPattern(string expression, ReadOnlySpan<char> name, EnumerationOptions options) + { + switch (options.MatchType) + { + case MatchType.Simple: + return FileSystemName.MatchesSimpleExpression(expression, name, ignoreCase: !PathInternal.IsCaseSensitive); + case MatchType.Dos: + return FileSystemName.MatchesDosExpression(expression, name, ignoreCase: !PathInternal.IsCaseSensitive); + default: + throw new ArgumentOutOfRangeException(nameof(options)); + } + } + + internal static IEnumerable<string> UserFiles(string directory, + string expression, + EnumerationOptions options) + { + return new FileSystemEnumerable<string>( + directory, + (ref FileSystemEntry entry) => entry.ToSpecifiedFullPath(), + options) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => + !entry.IsDirectory && MatchesPattern(expression, entry.FileName, options) + }; + } + + internal static IEnumerable<string> UserDirectories(string directory, + string expression, + EnumerationOptions options) + { + return new FileSystemEnumerable<string>( + directory, + (ref FileSystemEntry entry) => entry.ToSpecifiedFullPath(), + options) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => + entry.IsDirectory && MatchesPattern(expression, entry.FileName, options) + }; + } + + internal static IEnumerable<string> UserEntries(string directory, + string expression, + EnumerationOptions options) + { + return new FileSystemEnumerable<string>( + directory, + (ref FileSystemEntry entry) => entry.ToSpecifiedFullPath(), + options) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => + MatchesPattern(expression, entry.FileName, options) + }; + } + + internal static IEnumerable<FileInfo> FileInfos( + string directory, + string expression, + EnumerationOptions options) + { + return new FileSystemEnumerable<FileInfo>( + directory, + (ref FileSystemEntry entry) => (FileInfo)entry.ToFileSystemInfo(), + options) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => + !entry.IsDirectory && MatchesPattern(expression, entry.FileName, options) + }; + } + + internal static IEnumerable<DirectoryInfo> DirectoryInfos( + string directory, + string expression, + EnumerationOptions options) + { + return new FileSystemEnumerable<DirectoryInfo>( + directory, + (ref FileSystemEntry entry) => (DirectoryInfo)entry.ToFileSystemInfo(), + options) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => + entry.IsDirectory && MatchesPattern(expression, entry.FileName, options) + }; + } + + internal static IEnumerable<FileSystemInfo> FileSystemInfos( + string directory, + string expression, + EnumerationOptions options) + { + return new FileSystemEnumerable<FileSystemInfo>( + directory, + (ref FileSystemEntry entry) => entry.ToFileSystemInfo(), + options) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => + MatchesPattern(expression, entry.FileName, options) + }; + } + } +} 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 new file mode 100644 index 0000000000..eaadea9bcb --- /dev/null +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Unix.cs @@ -0,0 +1,191 @@ +// 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.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> + { + private readonly string _originalRootDirectory; + private readonly string _rootDirectory; + private readonly EnumerationOptions _options; + + private readonly object _lock = new object(); + + private string _currentPath; + private SafeDirectoryHandle _directoryHandle; + private bool _lastEntryFound; + private Queue<(SafeDirectoryHandle Handle, string Path)> _pending; + + private Interop.Sys.DirectoryEntry _entry; + private TResult _current; + + /// <summary> + /// Encapsulates a find operation. + /// </summary> + /// <param name="directory">The directory to search in.</param> + /// <param name="options">Enumeration options to use.</param> + public FileSystemEnumerator(string directory, EnumerationOptions options = null) + { + _originalRootDirectory = directory ?? throw new ArgumentNullException(nameof(directory)); + _rootDirectory = Path.GetFullPath(directory); + _options = options ?? EnumerationOptions.Default; + + // 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 == null) + _lastEntryFound = true; + + _currentPath = _rootDirectory; + } + + private static SafeDirectoryHandle CreateDirectoryHandle(string path) + { + // 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); + if (handle.IsInvalid) + { + throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), path, isDirectory: true); + } + return handle; + } + + private void CloseDirectoryHandle() + { + _directoryHandle?.Dispose(); + _directoryHandle = null; + } + + public bool MoveNext() + { + if (_lastEntryFound) + return false; + + FileSystemEntry entry = default; + + lock (_lock) + { + if (_lastEntryFound) + return false; + + do + { + FindNextEntry(); + 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)); + } + + 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)) + 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 + if (PathHelpers.IsDotOrDotDot(_entry.InodeName)) + { + // "." or "..", don't process unless the option is set + if (!_options.ReturnSpecialDirectories) + continue; + } + else if (_options.RecurseSubdirectories && ShouldRecurseIntoEntry(ref entry)) + { + // Recursion is on and the directory was accepted, Queue it + string subdirectory = PathHelpers.CombineNoChecks(_currentPath, _entry.InodeName); + SafeDirectoryHandle subdirectoryHandle = CreateDirectoryHandle(subdirectory); + if (subdirectoryHandle != null) + { + try + { + if (_pending == null) + _pending = new Queue<(SafeDirectoryHandle, string)>(); + _pending.Enqueue((subdirectoryHandle, subdirectory)); + } + catch + { + // Couldn't queue the handle, close it and rethrow + subdirectoryHandle.Dispose(); + throw; + } + } + } + } + + if (ShouldIncludeEntry(ref entry)) + { + _current = TransformEntry(ref entry); + return true; + } + } while (true); + } + } + + private unsafe void FindNextEntry() + { + // Read each entry from the enumerator + if (Interop.Sys.ReadDir(_directoryHandle, out _entry) != 0) + { + // TODO: https://github.com/dotnet/corefx/issues/26715 + // - Refactor ReadDir so we can process errors here + + // Directory finished + DirectoryFinished(); + } + } + + 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; + + CloseDirectoryHandle(); + + if (_pending != null) + { + while (_pending.Count > 0) + _pending.Dequeue().Handle.Dispose(); + _pending = null; + } + } + } + + Dispose(disposing); + } + } +} diff --git a/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Win32.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Win32.cs index a978d7cb85..57d9cbfae0 100644 --- a/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Win32.cs +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Win32.cs @@ -5,10 +5,11 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -namespace System.IO +namespace System.IO.Enumeration { - internal partial class FindEnumerable<TResult, TState> + public partial class FileSystemEnumerator<TResult> { + /// <returns>'true' if new data was found</returns> [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe bool GetData() { @@ -36,7 +37,15 @@ namespace System.IO Debug.Assert(statusBlock.Information.ToInt64() != 0); return true; default: - throw Win32Marshal.GetExceptionForWin32Error((int)Interop.NtDll.RtlNtStatusToDosError(status), _currentPath); + int error = (int)Interop.NtDll.RtlNtStatusToDosError(status); + + // Note that there are many NT status codes that convert to ERROR_ACCESS_DENIED. + if ((error == Interop.Errors.ERROR_ACCESS_DENIED && _options.IgnoreInaccessible) || ContinueOnError(error)) + { + DirectoryFinished(); + return false; + } + throw Win32Marshal.GetExceptionForWin32Error(error, _currentPath); } } } diff --git a/src/System.IO.FileSystem/src/System/IO/FindEnumerable.WinRT.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.WinRT.cs index 61e8b05d4d..ee57114803 100644 --- a/src/System.IO.FileSystem/src/System/IO/FindEnumerable.WinRT.cs +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.WinRT.cs @@ -5,9 +5,9 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace System.IO +namespace System.IO.Enumeration { - internal partial class FindEnumerable<TResult, TState> + public partial class FileSystemEnumerator<TResult> { [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe bool GetData() @@ -24,9 +24,16 @@ namespace System.IO case Interop.Errors.ERROR_NO_MORE_FILES: DirectoryFinished(); return false; - default: - throw Win32Marshal.GetExceptionForWin32Error(error, _currentPath); + case Interop.Errors.ERROR_ACCESS_DENIED: + if (_options.IgnoreInaccessible) + { + return false; + } + break; } + + if (!ContinueOnError(error)) + throw Win32Marshal.GetExceptionForWin32Error(error, _currentPath); } return true; diff --git a/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Windows.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Windows.cs new file mode 100644 index 0000000000..18fa129367 --- /dev/null +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Windows.cs @@ -0,0 +1,230 @@ +// 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; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Threading; + +namespace System.IO.Enumeration +{ + public unsafe abstract partial class FileSystemEnumerator<TResult> : CriticalFinalizerObject, IEnumerator<TResult> + { + private const int StandardBufferSize = 4096; + + // We need to have enough room for at least a single entry. The filename alone can be 512 bytes, we'll ensure we have + // a reasonable buffer for all of the other metadata as well. + private const int MinimumBufferSize = 1024; + + private readonly string _originalRootDirectory; + private readonly string _rootDirectory; + private readonly EnumerationOptions _options; + + private readonly object _lock = new object(); + + private Interop.NtDll.FILE_FULL_DIR_INFORMATION* _entry; + private TResult _current; + + private byte[] _buffer; + private IntPtr _directoryHandle; + private string _currentPath; + private bool _lastEntryFound; + private Queue<(IntPtr Handle, string Path)> _pending; + private GCHandle _pinnedBuffer; + + /// <summary> + /// Encapsulates a find operation. + /// </summary> + /// <param name="directory">The directory to search in.</param> + /// <param name="options">Enumeration options to use.</param> + public FileSystemEnumerator(string directory, EnumerationOptions options = null) + { + _originalRootDirectory = directory ?? throw new ArgumentNullException(nameof(directory)); + _rootDirectory = Path.GetFullPath(directory); + _options = options ?? EnumerationOptions.Default; + + // We'll only suppress the media insertion prompt on the topmost directory as that is the + // most likely scenario and we don't want to take the perf hit for large enumerations. + // (We weren't consistent with how we handled this historically.) + using (new DisableMediaInsertionPrompt()) + { + // 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; + + int requestedBufferSize = _options.BufferSize; + int bufferSize = requestedBufferSize <= 0 ? StandardBufferSize + : Math.Max(MinimumBufferSize, requestedBufferSize); + + try + { + _buffer = ArrayPool<byte>.Shared.Rent(bufferSize); + _pinnedBuffer = GCHandle.Alloc(_buffer, GCHandleType.Pinned); + } + catch + { + // Close the directory handle right away if we fail to allocate + CloseDirectoryHandle(); + throw; + } + } + + private void CloseDirectoryHandle() + { + // As handles can be reused we want to be extra careful to close handles only once + IntPtr handle = Interlocked.Exchange(ref _directoryHandle, IntPtr.Zero); + if (handle != IntPtr.Zero) + Interop.Kernel32.CloseHandle(handle); + } + + /// <summary> + /// Simple wrapper to allow creating a file handle for an existing directory. + /// </summary> + private IntPtr CreateDirectoryHandle(string path) + { + IntPtr handle = Interop.Kernel32.CreateFile_IntPtr( + path, + Interop.Kernel32.FileOperations.FILE_LIST_DIRECTORY, + FileShare.ReadWrite | FileShare.Delete, + FileMode.Open, + Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS); + + if (handle == IntPtr.Zero || handle == (IntPtr)(-1)) + { + int error = Marshal.GetLastWin32Error(); + + if ((error == Interop.Errors.ERROR_ACCESS_DENIED && + _options.IgnoreInaccessible) || ContinueOnError(error)) + { + return IntPtr.Zero; + } + + if (error == Interop.Errors.ERROR_FILE_NOT_FOUND) + { + // Historically we throw directory not found rather than file not found + error = Interop.Errors.ERROR_PATH_NOT_FOUND; + } + + throw Win32Marshal.GetExceptionForWin32Error(error, path); + } + + return handle; + } + + public bool MoveNext() + { + if (_lastEntryFound) + return false; + + FileSystemEntry entry = default; + + lock (_lock) + { + if (_lastEntryFound) + return false; + + do + { + FindNextEntry(); + if (_lastEntryFound) + return false; + + // Calling the constructor inside the try block would create a second instance on the stack. + FileSystemEntry.Initialize(ref entry, _entry, _currentPath, _rootDirectory, _originalRootDirectory); + + // Skip specified attributes + if ((_entry->FileAttributes & _options.AttributesToSkip) != 0) + continue; + + if ((_entry->FileAttributes & FileAttributes.Directory) != 0) + { + // Subdirectory found + if (PathHelpers.IsDotOrDotDot(_entry->FileName)) + { + // "." or "..", don't process unless the option is set + if (!_options.ReturnSpecialDirectories) + continue; + } + else if (_options.RecurseSubdirectories && ShouldRecurseIntoEntry(ref entry)) + { + // Recursion is on and the directory was accepted, Queue it + string subDirectory = PathHelpers.CombineNoChecks(_currentPath, _entry->FileName); + IntPtr subDirectoryHandle = CreateDirectoryHandle(subDirectory); + if (subDirectoryHandle != IntPtr.Zero) + { + try + { + if (_pending == null) + _pending = new Queue<(IntPtr, string)>(); + _pending.Enqueue((subDirectoryHandle, subDirectory)); + } + catch + { + // Couldn't queue the handle, close it and rethrow + Interop.Kernel32.CloseHandle(subDirectoryHandle); + throw; + } + } + } + } + + if (ShouldIncludeEntry(ref entry)) + { + _current = TransformEntry(ref entry); + return true; + } + } while (true); + } + } + + private unsafe void FindNextEntry() + { + _entry = Interop.NtDll.FILE_FULL_DIR_INFORMATION.GetNextInfo(_entry); + if (_entry != null) + return; + + // We need more data + if (GetData()) + _entry = (Interop.NtDll.FILE_FULL_DIR_INFORMATION*)_pinnedBuffer.AddrOfPinnedObject(); + } + + 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; + + CloseDirectoryHandle(); + + if (_pending != null) + { + while (_pending.Count > 0) + Interop.Kernel32.CloseHandle(_pending.Dequeue().Handle); + _pending = null; + } + + if (_pinnedBuffer.IsAllocated) + _pinnedBuffer.Free(); + + if (_buffer != null) + ArrayPool<byte>.Shared.Return(_buffer); + + _buffer = null; + } + } + + Dispose(disposing); + } + } +} diff --git a/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.cs new file mode 100644 index 0000000000..01bcd7babd --- /dev/null +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.cs @@ -0,0 +1,89 @@ +// 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.Collections; +using System.Collections.Generic; +using System.Runtime.ConstrainedExecution; + +namespace System.IO.Enumeration +{ + public unsafe abstract partial class FileSystemEnumerator<TResult> : CriticalFinalizerObject, IEnumerator<TResult> + { + /// <summary> + /// Return true if the given file system entry should be included in the results. + /// </summary> + protected virtual bool ShouldIncludeEntry(ref FileSystemEntry entry) => true; + + /// <summary> + /// Return true if the directory entry given should be recursed into. + /// </summary> + protected virtual bool ShouldRecurseIntoEntry(ref FileSystemEntry entry) => true; + + /// <summary> + /// Generate the result type from the current entry; + /// </summary> + protected abstract TResult TransformEntry(ref FileSystemEntry entry); + + /// <summary> + /// Called whenever the end of a directory is reached. + /// </summary> + /// <param name="directory">The path of the directory that finished.</param> + protected virtual void OnDirectoryFinished(ReadOnlySpan<char> directory) { } + + /// <summary> + /// Called when a native API returns an error. Return true to continue, or false + /// to throw the default exception for the given error. + /// </summary> + /// <param name="error">The native error code.</param> + protected virtual bool ContinueOnError(int error) => false; + + public TResult Current => _current; + + object IEnumerator.Current => Current; + + private void DirectoryFinished() + { + _entry = default; + + // Close the handle now that we're done + CloseDirectoryHandle(); + OnDirectoryFinished(_currentPath); + + if (_pending == null || _pending.Count == 0) + { + _lastEntryFound = true; + } + else + { + // Grab the next directory to parse + (_directoryHandle, _currentPath) = _pending.Dequeue(); + FindNextEntry(); + } + } + + public void Reset() + { + throw new NotSupportedException(); + } + + public void Dispose() + { + InternalDispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Override for any additional cleanup. + /// </summary> + /// <param name="disposing">True if called while disposing. False if called from finalizer.</param> + protected virtual void Dispose(bool disposing) + { + } + + ~FileSystemEnumerator() + { + InternalDispose(disposing: false); + } + } +} diff --git a/src/System.IO.FileSystem/src/System/IO/DosMatcher.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemName.cs index b1f677efb6..dfdd1dd42f 100644 --- a/src/System.IO.FileSystem/src/System/IO/DosMatcher.cs +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemName.cs @@ -5,9 +5,12 @@ using System; using System.Text; -namespace System.IO +namespace System.IO.Enumeration { - internal static class DosMatcher + /// <summary> + /// Provides methods for matching file system names. + /// </summary> + public static class FileSystemName { // [MS - FSA] 2.1.4.4 Algorithm for Determining if a FileName Is in an Expression // https://msdn.microsoft.com/en-us/library/ff469270.aspx @@ -16,11 +19,16 @@ namespace System.IO '\"', '<', '>', '*', '?' }; + private static readonly char[] s_simpleWildcardChars = + { + '*', '?' + }; + /// <summary> /// Change '*' and '?' to '<', '>' and '"' to match Win32 behavior. For compatibility, Windows /// changes some wildcards to provide a closer match to historical DOS 8.3 filename matching. /// </summary> - internal static string TranslateExpression(string expression) + public static string TranslateDosExpression(string expression) { if (string.IsNullOrEmpty(expression) || expression == "*" || expression == "*.*") return "*"; @@ -64,7 +72,8 @@ namespace System.IO } /// <summary> - /// Return true if the given expression matches the given name. + /// Return true if the given expression matches the given name. Supports the following wildcards: + /// '*', '?', '<', '>', '"'. The backslash character '\' escapes. /// </summary> /// <param name="expression">The expression to match with, such as "*.foo".</param> /// <param name="name">The name to check against the expression.</param> @@ -74,16 +83,29 @@ namespace System.IO /// of RtlIsNameInExpression, which defines the rules for matching DOS wildcards ('*', '?', '<', '>', '"'). /// /// Like PatternMatcher, matching will not line up with Win32 behavior unless you transform the expression - /// using <see cref="TranslateExpression(string)"/> + /// using <see cref="TranslateDosExpression(string)"/> /// </remarks> - internal static bool MatchPattern(string expression, ReadOnlySpan<char> name, bool ignoreCase = true) + public static bool MatchesDosExpression(ReadOnlySpan<char> expression, ReadOnlySpan<char> name, bool ignoreCase = true) + { + return MatchPattern(expression, name, ignoreCase, useExtendedWildcards: true); + } + + /// <summary> + /// Return true if the given expression matches the given name. '*' and '?' are wildcards, '\' escapes. + /// </summary> + public static bool MatchesSimpleExpression(ReadOnlySpan<char> expression, ReadOnlySpan<char> name, bool ignoreCase = true) + { + return MatchPattern(expression, name, ignoreCase, useExtendedWildcards: false); + } + + private static bool MatchPattern(ReadOnlySpan<char> expression, ReadOnlySpan<char> name, bool ignoreCase, bool useExtendedWildcards) { // The idea behind the algorithm is pretty simple. We keep track of all possible locations // in the regular expression that are matching the name. When the name has been exhausted, // if one of the locations in the expression is also just exhausted, the name is in the // language defined by the regular expression. - if (string.IsNullOrEmpty(expression) || name.Length == 0) + if (expression.Length == 0 || name.Length == 0) return false; if (expression[0] == '*') @@ -92,16 +114,17 @@ namespace System.IO if (expression.Length == 1) return true; - if (expression.IndexOfAny(s_wildcardChars, startIndex: 1) == -1) + ReadOnlySpan<char> expressionEnd = expression.Slice(1); + if (expressionEnd.IndexOfAny(useExtendedWildcards ? s_wildcardChars : s_simpleWildcardChars) == -1) { // Handle the special case of a single starting *, which essentially means "ends with" // If the name doesn't have enough characters to match the remaining expression, it can't be a match. - if (name.Length < expression.Length - 1) + if (name.Length < expressionEnd.Length) return false; - // See if we end with the expression (minus the *, of course) - return name.EndsWithOrdinal(expression.AsReadOnlySpan().Slice(1), ignoreCase); + // See if we end with the expression + return name.EndsWithOrdinal(expressionEnd, ignoreCase); } } @@ -124,6 +147,26 @@ namespace System.IO int currentState; bool nameFinished = false; + // Walk through the name string, picking off characters. We go one + // character beyond the end because some wild cards are able to match + // zero characters beyond the end of the string. + // + // With each new name character we determine a new set of states that + // match the name so far. We use two arrays that we swap back and forth + // for this purpose. One array lists the possible expression states for + // all name characters up to but not including the current one, and other + // array is used to build up the list of states considering the current + // name character as well. The arrays are then switched and the process + // repeated. + // + // There is not a one-to-one correspondence between state number and + // offset into the expression. State numbering is not continuous. + // This allows a simple conversion between state number and expression + // offset. Each character in the expression can represent one or two + // states. * and DOS_STAR generate two states: expressionOffset * 2 and + // expressionOffset * 2 + 1. All other expression characters can produce + // only a single state. Thus expressionOffset = currentState / 2. + while (!nameFinished) { if (nameOffset < name.Length) @@ -177,7 +220,7 @@ namespace System.IO // '*' matches any character zero or more times. goto MatchZeroOrMore; } - else if (expressionChar == '<') + else if (useExtendedWildcards && expressionChar == '<') { // '<' (DOS_STAR) matches any character except '.' zero or more times. @@ -210,11 +253,11 @@ namespace System.IO } else { - // The following expression characters all match by consuming - // a character, thus force the expression, and thus state forward. + // The remaining expression characters all match by consuming a character, + // so we need to force the expression and state forward. currentState += 2; - if (expressionChar == '>') + if (useExtendedWildcards && expressionChar == '>') { // '>' (DOS_QM) is the most complicated. If the name is finished, // we can match zero characters. If this name is a '.', we @@ -226,7 +269,7 @@ namespace System.IO currentMatches[currentMatch++] = currentState; goto ExpressionFinished; } - else if (expressionChar == '"') + else if (useExtendedWildcards && expressionChar == '"') { // A '"' (DOS_DOT) can match either a period, or zero characters // beyond the end of name. @@ -242,6 +285,19 @@ namespace System.IO } else { + if (expressionChar == '\\') + { + // Escape character, try to move the expression forward again and match literally. + if (++expressionOffset == expression.Length) + { + currentMatches[currentMatch++] = maxState; + goto ExpressionFinished; + } + + currentState = expressionOffset * 2 + 2; + expressionChar = expression[expressionOffset]; + } + // From this point on a name character is required to even // continue, let alone make a match. if (nameFinished) goto ExpressionFinished; @@ -259,7 +315,6 @@ namespace System.IO currentMatches[currentMatch++] = currentState; } - // The expression didn't match so move to the next prior match. goto ExpressionFinished; } } diff --git a/src/System.IO.FileSystem/src/System/IO/Enumeration/MatchType.cs b/src/System.IO.FileSystem/src/System/IO/Enumeration/MatchType.cs new file mode 100644 index 0000000000..edd35620bc --- /dev/null +++ b/src/System.IO.FileSystem/src/System/IO/Enumeration/MatchType.cs @@ -0,0 +1,20 @@ +// 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.Enumeration +{ + public enum MatchType + { + /// <summary> + /// Match using '*' and '?' wildcards. + /// </summary> + Simple, + + /// <summary> + /// Match using DOS style matching semantics. '*', '?', '<', '>', and '"' + /// are all considered wildcards. + /// </summary> + Dos + } +} diff --git a/src/System.IO.FileSystem/src/System/IO/FileInfo.Windows.cs b/src/System.IO.FileSystem/src/System/IO/FileInfo.Windows.cs index cec962f3f4..8820620599 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileInfo.Windows.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileInfo.Windows.cs @@ -2,17 +2,17 @@ // 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.Diagnostics; +using System.IO.Enumeration; namespace System.IO { partial class FileInfo { - internal unsafe FileInfo(string fullPath, string fileName, ref RawFindData findData) - : this(fullPath, fileName: fileName, isNormalized: true) + internal static unsafe FileInfo Create(string fullPath, ref FileSystemEntry findData) { - Debug.Assert(fileName.Equals(Path.GetFileName(fullPath))); - Init(findData._info); + FileInfo info = new FileInfo(fullPath, fileName: findData.FileName.GetStringFromFixedBuffer(), isNormalized: true); + info.Init(findData._info); + return info; } } } diff --git a/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs b/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs index 955d568322..013c09e371 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileSystem.Unix.cs @@ -2,11 +2,8 @@ // 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.Collections; using System.Collections.Generic; using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; namespace System.IO { @@ -337,7 +334,7 @@ namespace System.IO { try { - foreach (string item in EnumeratePaths(directory.FullName, "*", SearchOption.TopDirectoryOnly, SearchTarget.Both)) + foreach (string item in Directory.EnumerateFileSystemEntries(directory.FullName)) { if (!ShouldIgnoreDirectory(Path.GetFileName(item))) { @@ -444,249 +441,6 @@ namespace System.IO ((fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR); } - public static IEnumerable<string> EnumeratePaths(string path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) - { - return new FileSystemEnumerable<string>(path, searchPattern, searchOption, searchTarget, (p, _) => p); - } - - public static IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string fullPath, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) - { - switch (searchTarget) - { - case SearchTarget.Files: - return new FileSystemEnumerable<FileInfo>(fullPath, searchPattern, searchOption, searchTarget, (path, isDir) => - { - var info = new FileInfo(path, null); - info.Refresh(); - return info; - }); - case SearchTarget.Directories: - return new FileSystemEnumerable<DirectoryInfo>(fullPath, searchPattern, searchOption, searchTarget, (path, isDir) => - { - var info = new DirectoryInfo(path, null); - info.Refresh(); - return info; - }); - default: - return new FileSystemEnumerable<FileSystemInfo>(fullPath, searchPattern, searchOption, searchTarget, (path, isDir) => - { - var info = isDir ? - (FileSystemInfo)new DirectoryInfo(path, null) : - (FileSystemInfo)new FileInfo(path, null); - info.Refresh(); - return info; - }); - } - } - - private sealed class FileSystemEnumerable<T> : IEnumerable<T> - { - private readonly PathPair _initialDirectory; - private readonly string _searchPattern; - private readonly SearchOption _searchOption; - private readonly bool _includeFiles; - private readonly bool _includeDirectories; - private readonly Func<string, bool, T> _translateResult; - private IEnumerator<T> _firstEnumerator; - - internal FileSystemEnumerable( - string userPath, string searchPattern, - SearchOption searchOption, SearchTarget searchTarget, - Func<string, bool, T> translateResult) - { - // Basic validation of the input path - if (userPath == null) - { - throw new ArgumentNullException("path"); - } - if (string.IsNullOrEmpty(userPath)) - { - throw new ArgumentException(SR.Argument_EmptyPath, "path"); - } - - // Validate and normalize the search pattern. If after doing so it's empty, - // matching Win32 behavior we can skip all additional validation and effectively - // return an empty enumerable. - searchPattern = NormalizeSearchPattern(searchPattern); - if (searchPattern.Length > 0) - { - PathHelpers.ThrowIfEmptyOrRootedPath(searchPattern); - - // If the search pattern contains any paths, make sure we factor those into - // the user path, and then trim them off. - int lastSlash = searchPattern.LastIndexOf(Path.DirectorySeparatorChar); - if (lastSlash >= 0) - { - if (lastSlash >= 1) - { - userPath = Path.Combine(userPath, searchPattern.Substring(0, lastSlash)); - } - searchPattern = searchPattern.Substring(lastSlash + 1); - } - - // Typically we shouldn't see either of these cases, an upfront check is much faster - foreach (char c in searchPattern) - { - if (c == '\\' || c == '[') - { - // We need to escape any escape characters in the search pattern - searchPattern = searchPattern.Replace(@"\", @"\\"); - - // And then escape '[' to prevent it being picked up as a wildcard - searchPattern = searchPattern.Replace(@"[", @"\["); - break; - } - } - - string fullPath = Path.GetFullPath(userPath); - - // Store everything for the enumerator - _initialDirectory = new PathPair(userPath, fullPath); - _searchPattern = searchPattern; - _searchOption = searchOption; - _includeFiles = (searchTarget & SearchTarget.Files) != 0; - _includeDirectories = (searchTarget & SearchTarget.Directories) != 0; - _translateResult = translateResult; - } - - // Open the first enumerator so that any errors are propagated synchronously. - _firstEnumerator = Enumerate(); - } - - public IEnumerator<T> GetEnumerator() - { - return Interlocked.Exchange(ref _firstEnumerator, null) ?? Enumerate(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - private IEnumerator<T> Enumerate() - { - return Enumerate( - _initialDirectory.FullPath != null ? - OpenDirectory(_initialDirectory.FullPath) : - null); - } - - private IEnumerator<T> Enumerate(Microsoft.Win32.SafeHandles.SafeDirectoryHandle dirHandle) - { - if (dirHandle == null) - { - // Empty search - yield break; - } - - Debug.Assert(!dirHandle.IsInvalid); - Debug.Assert(!dirHandle.IsClosed); - - // Maintain a stack of the directories to explore, in the case of SearchOption.AllDirectories - // Lazily-initialized only if we find subdirectories that will be explored. - Stack<PathPair> toExplore = null; - PathPair dirPath = _initialDirectory; - while (dirHandle != null) - { - try - { - // Read each entry from the enumerator - Interop.Sys.DirectoryEntry dirent; - while (Interop.Sys.ReadDir(dirHandle, out dirent) == 0) - { - // 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. - bool isDir; - if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR) - { - // We know it's a directory. - isDir = true; - } - else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.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. - Interop.ErrorInfo errnoIgnored; - isDir = DirectoryExists(Path.Combine(dirPath.FullPath, dirent.InodeName), out errnoIgnored); - } - else - { - // Otherwise, treat it as a file. This includes regular files, FIFOs, etc. - isDir = false; - } - - // Yield the result if the user has asked for it. In the case of directories, - // always explore it by pushing it onto the stack, regardless of whether - // we're returning directories. - if (isDir) - { - if (!ShouldIgnoreDirectory(dirent.InodeName)) - { - string userPath = null; - if (_searchOption == SearchOption.AllDirectories) - { - if (toExplore == null) - { - toExplore = new Stack<PathPair>(); - } - userPath = Path.Combine(dirPath.UserPath, dirent.InodeName); - toExplore.Push(new PathPair(userPath, Path.Combine(dirPath.FullPath, dirent.InodeName))); - } - if (_includeDirectories && - Interop.Sys.FnMatch(_searchPattern, dirent.InodeName, Interop.Sys.FnMatchFlags.FNM_NONE) == 0) - { - yield return _translateResult(userPath ?? Path.Combine(dirPath.UserPath, dirent.InodeName), /*isDirectory*/true); - } - } - } - else if (_includeFiles && - Interop.Sys.FnMatch(_searchPattern, dirent.InodeName, Interop.Sys.FnMatchFlags.FNM_NONE) == 0) - { - yield return _translateResult(Path.Combine(dirPath.UserPath, dirent.InodeName), /*isDirectory*/false); - } - } - } - finally - { - // Close the directory enumerator - dirHandle.Dispose(); - dirHandle = null; - } - - if (toExplore != null && toExplore.Count > 0) - { - // Open the next directory. - dirPath = toExplore.Pop(); - dirHandle = OpenDirectory(dirPath.FullPath); - } - } - } - - private static string NormalizeSearchPattern(string searchPattern) - { - if (searchPattern == "." || searchPattern == "*.*") - { - searchPattern = "*"; - } - else if (PathHelpers.EndsInDirectorySeparator(searchPattern)) - { - searchPattern += "*"; - } - - return searchPattern; - } - - private static Microsoft.Win32.SafeHandles.SafeDirectoryHandle OpenDirectory(string fullPath) - { - Microsoft.Win32.SafeHandles.SafeDirectoryHandle handle = Interop.Sys.OpenDir(fullPath); - if (handle.IsInvalid) - { - throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), fullPath, isDirectory: true); - } - return handle; - } - } - /// <summary>Determines whether the specified directory name should be ignored.</summary> /// <param name="name">The name to evaluate.</param> /// <returns>true if the name is "." or ".."; otherwise, false.</returns> @@ -762,13 +516,6 @@ namespace System.IO info.LastWriteTimeCore = time; } - public static FileSystemInfo GetFileSystemInfo(string fullPath, bool asDirectory) - { - return asDirectory ? - (FileSystemInfo)new DirectoryInfo(fullPath, null) : - (FileSystemInfo)new FileInfo(fullPath, null); - } - public static string[] GetLogicalDrives() { return DriveInfoInternal.GetLogicalDrives(); diff --git a/src/System.IO.FileSystem/src/System/IO/FileSystem.Windows.cs b/src/System.IO.FileSystem/src/System/IO/FileSystem.Windows.cs index 84f8e6ff87..17bb79d37e 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileSystem.Windows.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileSystem.Windows.cs @@ -183,38 +183,6 @@ namespace System.IO && ((data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0); } - public static IEnumerable<string> EnumeratePaths(string fullPath, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) - { - FindEnumerableFactory.NormalizeInputs(ref fullPath, ref searchPattern); - switch (searchTarget) - { - case SearchTarget.Files: - return FindEnumerableFactory.UserFiles(fullPath, searchPattern, searchOption == SearchOption.AllDirectories); - case SearchTarget.Directories: - return FindEnumerableFactory.UserDirectories(fullPath, searchPattern, searchOption == SearchOption.AllDirectories); - case SearchTarget.Both: - return FindEnumerableFactory.UserEntries(fullPath, searchPattern, searchOption == SearchOption.AllDirectories); - default: - throw new ArgumentOutOfRangeException(nameof(searchTarget)); - } - } - - public static IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string fullPath, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) - { - FindEnumerableFactory.NormalizeInputs(ref fullPath, ref searchPattern); - switch (searchTarget) - { - case SearchTarget.Directories: - return FindEnumerableFactory.DirectoryInfos(fullPath, searchPattern, searchOption == SearchOption.AllDirectories); - case SearchTarget.Files: - return FindEnumerableFactory.FileInfos(fullPath, searchPattern, searchOption == SearchOption.AllDirectories); - case SearchTarget.Both: - return FindEnumerableFactory.FileSystemInfos(fullPath, searchPattern, searchOption == SearchOption.AllDirectories); - default: - throw new ArgumentException(SR.ArgumentOutOfRange_Enum, nameof(searchTarget)); - } - } - /// <summary> /// Returns 0 on success, otherwise a Win32 error code. Note that /// classes should use -1 as the uninitialized state for dataInitialized. diff --git a/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Windows.cs b/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Windows.cs deleted file mode 100644 index 7e64e97373..0000000000 --- a/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Windows.cs +++ /dev/null @@ -1,251 +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. - -using System.Buffers; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.ConstrainedExecution; -using System.Runtime.InteropServices; -using System.Threading; - -namespace System.IO -{ - internal unsafe partial class FindEnumerable<TResult, TState> : CriticalFinalizerObject, IEnumerable<TResult>, IEnumerator<TResult> - { - private readonly string _originalFullPath; - private readonly string _originalUserPath; - private readonly bool _recursive; - private readonly FindTransform<TResult> _transform; - private readonly FindPredicate<TState> _predicate; - private readonly TState _state; - - private object _lock = new object(); - private int _enumeratorCreated; - - private Interop.NtDll.FILE_FULL_DIR_INFORMATION* _info; - private TResult _current; - - private byte[] _buffer; - private IntPtr _directoryHandle; - private string _currentPath; - private bool _lastEntryFound; - private Queue<(IntPtr Handle, string Path)> _pending; - private GCHandle _pinnedBuffer; - - /// <summary> - /// Encapsulates a find operation. - /// </summary> - /// <param name="directory">The directory to search in.</param> - public FindEnumerable( - string directory, - FindTransform<TResult> transform, - FindPredicate<TState> predicate, - TState state = default, - bool recursive = false) - { - _originalUserPath = directory; - _originalFullPath = Path.GetFullPath(directory); - _recursive = recursive; - _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); - _transform = transform ?? throw new ArgumentNullException(nameof(transform)); - _state = state; - Initialize(); - } - - private FindEnumerable( - string originalUserPath, - string originalFullPath, - FindTransform<TResult> transform, - FindPredicate<TState> predicate, - TState state, - bool recursive) - { - _originalUserPath = originalUserPath; - _originalFullPath = originalFullPath; - _predicate = predicate; - _transform = transform; - _state = state; - _recursive = recursive; - Initialize(); - } - - /// <summary> - /// Simple wrapper to allow creating a file handle for an existing directory. - /// </summary> - public static IntPtr CreateDirectoryHandle(string path) - { - IntPtr handle = Interop.Kernel32.CreateFile_IntPtr( - path, - Interop.Kernel32.FileOperations.FILE_LIST_DIRECTORY, - FileShare.ReadWrite | FileShare.Delete, - FileMode.Open, - Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS); - - if (handle == IntPtr.Zero || handle == (IntPtr)(-1)) - { - // Historically we throw directory not found rather than file not found - int error = Marshal.GetLastWin32Error(); - if (error == Interop.Errors.ERROR_FILE_NOT_FOUND) - error = Interop.Errors.ERROR_PATH_NOT_FOUND; - - throw Win32Marshal.GetExceptionForWin32Error(error, path); - } - - return handle; - } - - public IEnumerator<TResult> GetEnumerator() - { - if (Interlocked.Exchange(ref _enumeratorCreated, 1) == 0) - { - return this; - } - else - { - return new FindEnumerable<TResult, TState>(_originalUserPath, _originalFullPath, _transform, _predicate, _state, _recursive); - } - } - - private void Initialize() - { - _currentPath = _originalFullPath; - _buffer = ArrayPool<byte>.Shared.Rent(4096); - _pinnedBuffer = GCHandle.Alloc(_buffer, GCHandleType.Pinned); - if (_recursive) - _pending = new Queue<(IntPtr, string)>(); - _directoryHandle = CreateDirectoryHandle(_originalFullPath); - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public TResult Current => _current; - - object IEnumerator.Current => Current; - - public bool MoveNext() - { - if (_lastEntryFound) - return false; - - lock (_lock) - { - if (_lastEntryFound) - return false; - - RawFindData findData = default; - do - { - FindNextFile(); - if (!_lastEntryFound && _info != null) - { - // If needed, stash any subdirectories to process later - if (_recursive && (_info->FileAttributes & FileAttributes.Directory) != 0 - && !PathHelpers.IsDotOrDotDot(_info->FileName)) - { - string subDirectory = PathHelpers.CombineNoChecks(_currentPath, _info->FileName); - IntPtr subDirectoryHandle = CreateDirectoryHandle(subDirectory); - try - { - // It is possible this might allocate and run out of memory - _pending.Enqueue((subDirectoryHandle, subDirectory)); - } - catch - { - Interop.Kernel32.CloseHandle(subDirectoryHandle); - throw; - } - } - - findData = new RawFindData(_info, _currentPath, _originalFullPath, _originalUserPath); - } - } while (!_lastEntryFound && !_predicate(ref findData, _state)); - - if (!_lastEntryFound) - _current = _transform(ref findData); - - return !_lastEntryFound; - } - } - - private unsafe void FindNextFile() - { - Interop.NtDll.FILE_FULL_DIR_INFORMATION* info = _info; - if (info != null && info->NextEntryOffset != 0) - { - // We're already in a buffer and have another entry - _info = (Interop.NtDll.FILE_FULL_DIR_INFORMATION*)((byte*)info + info->NextEntryOffset); - return; - } - - // We need more data - if (GetData()) - _info = (Interop.NtDll.FILE_FULL_DIR_INFORMATION*)_pinnedBuffer.AddrOfPinnedObject(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void DirectoryFinished() - { - _info = null; - if (_pending == null || _pending.Count == 0) - { - _lastEntryFound = true; - } - else - { - // Grab the next directory to parse - Interop.Kernel32.CloseHandle(_directoryHandle); - (_directoryHandle, _currentPath) = _pending.Dequeue(); - FindNextFile(); - } - } - - public void Reset() - { - throw new NotSupportedException(); - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - protected void Dispose(bool disposing) - { - // It is possible to fail to allocate the lock, but the finalizer will still run - if (_lock != null) - { - lock (_lock) - { - _lastEntryFound = true; - - // Don't ever close a valid handle twice as they can be reused- set to zero to ensure this - Interop.Kernel32.CloseHandle(_directoryHandle); - _directoryHandle = IntPtr.Zero; - - if (_recursive && _pending != null) - { - while (_pending.Count > 0) - Interop.Kernel32.CloseHandle(_pending.Dequeue().Handle); - _pending = null; - } - - if (_pinnedBuffer.IsAllocated) - _pinnedBuffer.Free(); - - if (_buffer != null) - ArrayPool<byte>.Shared.Return(_buffer); - - _buffer = null; - } - } - } - - ~FindEnumerable() - { - Dispose(disposing: false); - } - } -} diff --git a/src/System.IO.FileSystem/src/System/IO/FindEnumerableFactory.cs b/src/System.IO.FileSystem/src/System/IO/FindEnumerableFactory.cs deleted file mode 100644 index 1202ac7372..0000000000 --- a/src/System.IO.FileSystem/src/System/IO/FindEnumerableFactory.cs +++ /dev/null @@ -1,138 +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 -{ - internal static class FindEnumerableFactory - { - internal static void NormalizeInputs(ref string directory, ref string expression) - { - if (PathHelpers.IsPathRooted(expression)) - throw new ArgumentException(SR.Arg_Path2IsRooted, nameof(expression)); - - // We always allowed breaking the passed in directory and filter to be separated - // any way the user wanted. Looking for "C:\foo\*.cs" could be passed as "C:\" and - // "foo\*.cs" or "C:\foo" and "*.cs", for example. As such we need to combine and - // split the inputs if the expression contains a directory separator. - // - // We also allowed for expression to be "foo\" which would translate to "foo\*". - - ReadOnlySpan<char> directoryName = PathHelpers.GetDirectoryNameNoChecks(expression.AsReadOnlySpan()); - - if (directoryName.Length != 0) - { - // Need to fix up the input paths - directory = PathHelpers.CombineNoChecks(directory, directoryName); - expression = expression.Substring(directoryName.Length + 1); - } - - // Historically we always treated "." as "*" - if (string.IsNullOrEmpty(expression) || expression == "." || expression == "*.*") - expression = "*"; - } - - internal static FindEnumerable<string, string> UserFiles(string directory, - string expression = "*", - bool recursive = false) - { - return new FindEnumerable<string, string>( - directory, - (ref RawFindData findData) => FindTransforms.AsUserFullPath(ref findData), - (ref RawFindData findData, string expr) => - { - return FindPredicates.NotDotOrDotDot(ref findData) - && !FindPredicates.IsDirectory(ref findData) - && DosMatcher.MatchPattern(expr, findData.FileName, ignoreCase: true); - }, - DosMatcher.TranslateExpression(expression), - recursive); - } - - internal static FindEnumerable<string, string> UserDirectories(string directory, - string expression = "*", - bool recursive = false) - { - return new FindEnumerable<string, string>( - directory, - (ref RawFindData findData) => FindTransforms.AsUserFullPath(ref findData), - (ref RawFindData findData, string expr) => - { - return FindPredicates.NotDotOrDotDot(ref findData) - && FindPredicates.IsDirectory(ref findData) - && DosMatcher.MatchPattern(expr, findData.FileName, ignoreCase: true); - }, - DosMatcher.TranslateExpression(expression), - recursive); - } - - internal static FindEnumerable<string, string> UserEntries(string directory, - string expression = "*", - bool recursive = false) - { - return new FindEnumerable<string, string>( - directory, - (ref RawFindData findData) => FindTransforms.AsUserFullPath(ref findData), - (ref RawFindData findData, string expr) => - { - return FindPredicates.NotDotOrDotDot(ref findData) - && DosMatcher.MatchPattern(expr, findData.FileName, ignoreCase: true); - }, - DosMatcher.TranslateExpression(expression), - recursive); - } - - internal static FindEnumerable<FileInfo, string> FileInfos( - string directory, - string expression = "*", - bool recursive = false) - { - return new FindEnumerable<FileInfo, string>( - directory, - (ref RawFindData findData) => FindTransforms.AsFileInfo(ref findData), - (ref RawFindData findData, string expr) => - { - return FindPredicates.NotDotOrDotDot(ref findData) - && !FindPredicates.IsDirectory(ref findData) - && DosMatcher.MatchPattern(expr, findData.FileName, ignoreCase: true); - }, - DosMatcher.TranslateExpression(expression), - recursive); - } - - internal static FindEnumerable<DirectoryInfo, string> DirectoryInfos( - string directory, - string expression = "*", - bool recursive = false) - { - return new FindEnumerable<DirectoryInfo, string>( - directory, - (ref RawFindData findData) => FindTransforms.AsDirectoryInfo(ref findData), - (ref RawFindData findData, string expr) => - { - return FindPredicates.NotDotOrDotDot(ref findData) - && FindPredicates.IsDirectory(ref findData) - && DosMatcher.MatchPattern(expr, findData.FileName, ignoreCase: true); - }, - DosMatcher.TranslateExpression(expression), - recursive); - } - - internal static FindEnumerable<FileSystemInfo, string> FileSystemInfos( - string directory, - string expression = "*", - bool recursive = false) - { - return new FindEnumerable<FileSystemInfo, string>( - directory, - (ref RawFindData findData) => FindTransforms.AsFileSystemInfo(ref findData), - (ref RawFindData findData, string expr) => - { - return FindPredicates.NotDotOrDotDot(ref findData) - && DosMatcher.MatchPattern(expr, findData.FileName, ignoreCase: true); - }, - DosMatcher.TranslateExpression(expression), - recursive); - } - } -} diff --git a/src/System.IO.FileSystem/src/System/IO/FindPredicate.cs b/src/System.IO.FileSystem/src/System/IO/FindPredicate.cs deleted file mode 100644 index 2e52eda3db..0000000000 --- a/src/System.IO.FileSystem/src/System/IO/FindPredicate.cs +++ /dev/null @@ -1,11 +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 -{ - /// <summary> - /// Interface for filtering out find results. - /// </summary> - internal delegate bool FindPredicate<TState>(ref RawFindData findData, TState state); -} diff --git a/src/System.IO.FileSystem/src/System/IO/FindPredicates.cs b/src/System.IO.FileSystem/src/System/IO/FindPredicates.cs deleted file mode 100644 index 9d90aed377..0000000000 --- a/src/System.IO.FileSystem/src/System/IO/FindPredicates.cs +++ /dev/null @@ -1,18 +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 -{ - internal static partial class FindPredicates - { - internal static bool NotDotOrDotDot(ref RawFindData findData) => !PathHelpers.IsDotOrDotDot(findData.FileName); - - internal static bool IsDirectory(ref RawFindData findData) - { - FileAttributes attributes = findData.Attributes; - return attributes != (FileAttributes)(-1) - && (attributes & FileAttributes.Directory) != 0; - } - } -} diff --git a/src/System.IO.FileSystem/src/System/IO/FindTransform.cs b/src/System.IO.FileSystem/src/System/IO/FindTransform.cs deleted file mode 100644 index c1b6b71972..0000000000 --- a/src/System.IO.FileSystem/src/System/IO/FindTransform.cs +++ /dev/null @@ -1,11 +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 -{ - /// <summary> - /// Delegate for transforming raw find data into a result. - /// </summary> - internal delegate T FindTransform<T>(ref RawFindData findData); -} diff --git a/src/System.IO.FileSystem/src/System/IO/FindTransforms.cs b/src/System.IO.FileSystem/src/System/IO/FindTransforms.cs deleted file mode 100644 index db0590a35e..0000000000 --- a/src/System.IO.FileSystem/src/System/IO/FindTransforms.cs +++ /dev/null @@ -1,40 +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 -{ - internal static partial class FindTransforms - { - internal static DirectoryInfo AsDirectoryInfo(ref RawFindData findData) - { - string fileName = new string(findData.FileName); - return new DirectoryInfo(PathHelpers.CombineNoChecks(findData.Directory, fileName), fileName, ref findData); - } - - internal static FileInfo AsFileInfo(ref RawFindData findData) - { - string fileName = new string(findData.FileName); - return new FileInfo(PathHelpers.CombineNoChecks(findData.Directory, fileName), fileName, ref findData); - } - - internal static FileSystemInfo AsFileSystemInfo(ref RawFindData findData) - { - string fileName = new string(findData.FileName); - string fullPath = PathHelpers.CombineNoChecks(findData.Directory, fileName); - - return (findData.Attributes & FileAttributes.Directory) != 0 - ? (FileSystemInfo)new DirectoryInfo(fullPath, fileName, ref findData) - : (FileSystemInfo)new FileInfo(fullPath, fileName, ref findData); - } - - /// <summary> - /// Returns the full path for find results, based off of the initially provided path. - /// </summary> - internal static string AsUserFullPath(ref RawFindData findData) - { - ReadOnlySpan<char> subdirectory = findData.Directory.AsReadOnlySpan().Slice(findData.OriginalDirectory.Length); - return PathHelpers.CombineNoChecks(findData.OriginalUserDirectory, subdirectory, findData.FileName); - } - } -} diff --git a/src/System.IO.FileSystem/src/System/IO/PathHelpers.Windows.cs b/src/System.IO.FileSystem/src/System/IO/PathHelpers.Windows.cs index 990527577b..9cd0cf500d 100644 --- a/src/System.IO.FileSystem/src/System/IO/PathHelpers.Windows.cs +++ b/src/System.IO.FileSystem/src/System/IO/PathHelpers.Windows.cs @@ -30,19 +30,5 @@ namespace System.IO EndsInDirectorySeparator(path) ? path.Substring(0, path.Length - 1) : path; - - public static bool IsPathRooted(string path) - { - // Want to avoid PathInternal.CheckInvalidPathChars on Path.IsPathRooted - - if (path != null) - { - int length = path.Length; - if ((length >= 1 && PathInternal.IsDirectorySeparator(path[0])) || - (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == Path.VolumeSeparatorChar)) - return true; - } - return false; - } } } diff --git a/src/System.IO.FileSystem/src/System/IO/PathHelpers.cs b/src/System.IO.FileSystem/src/System/IO/PathHelpers.cs index dc574c7833..f6563651a1 100644 --- a/src/System.IO.FileSystem/src/System/IO/PathHelpers.cs +++ b/src/System.IO.FileSystem/src/System/IO/PathHelpers.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.IO.Enumeration; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/src/System.IO.FileSystem/src/System/IO/RawFindData.cs b/src/System.IO.FileSystem/src/System/IO/RawFindData.cs deleted file mode 100644 index 2f8ab43e42..0000000000 --- a/src/System.IO.FileSystem/src/System/IO/RawFindData.cs +++ /dev/null @@ -1,33 +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 -{ - /// <summary> - /// Used for processing and filtering find results. - /// </summary> - internal unsafe ref struct RawFindData - { - internal RawFindData(Interop.NtDll.FILE_FULL_DIR_INFORMATION* info, string directory, string originalDirectory, string originalUserDirectory) - { - _info = info; - Directory = directory; - OriginalDirectory = originalDirectory; - OriginalUserDirectory = originalUserDirectory; - } - - internal unsafe Interop.NtDll.FILE_FULL_DIR_INFORMATION* _info; - public string Directory { get; private set; } - public string OriginalDirectory { get; private set; } - public string OriginalUserDirectory { get; private set; } - - public ReadOnlySpan<char> FileName => _info->FileName; - public FileAttributes Attributes => _info->FileAttributes; - public long Length => _info->EndOfFile; - - public DateTime CreationTimeUtc => _info->CreationTime.ToDateTimeUtc(); - public DateTime LastAccessTimeUtc => _info->LastAccessTime.ToDateTimeUtc(); - public DateTime LastWriteTimeUtc => _info->LastWriteTime.ToDateTimeUtc(); - } -} diff --git a/src/System.IO.FileSystem/tests/Enumeration/ConstructionTests.netcoreapp.cs b/src/System.IO.FileSystem/tests/Enumeration/ConstructionTests.netcoreapp.cs new file mode 100644 index 0000000000..4f453200ee --- /dev/null +++ b/src/System.IO.FileSystem/tests/Enumeration/ConstructionTests.netcoreapp.cs @@ -0,0 +1,46 @@ +// 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 ConstructionTests : FileSystemTest + { + [Fact] + public void Enumerable_NullTransformThrows() + { + AssertExtensions.Throws<ArgumentNullException>("transform", + () => new FileSystemEnumerable<string>(TestDirectory, transform: null)); + } + + [Fact] + public void Enumerable_NullDirectoryThrows() + { + AssertExtensions.Throws<ArgumentNullException>("directory", + () => new FileSystemEnumerable<string>(null, null)); + } + + private class TestEnumerator : FileSystemEnumerator<string> + { + public TestEnumerator(string directory, EnumerationOptions options) + : base(directory, options) + { + } + + protected override string TransformEntry(ref FileSystemEntry entry) + { + throw new NotImplementedException(); + } + } + + [Fact] + public void Enumerator_NullDirectoryThrows() + { + AssertExtensions.Throws<ArgumentNullException>("directory", + () => new TestEnumerator(null, null)); + } + } +} diff --git a/src/System.IO.FileSystem/tests/Enumeration/DosMatcherTests.netcoreapp.cs b/src/System.IO.FileSystem/tests/Enumeration/DosMatcherTests.netcoreapp.cs new file mode 100644 index 0000000000..cdbfbcc27a --- /dev/null +++ b/src/System.IO.FileSystem/tests/Enumeration/DosMatcherTests.netcoreapp.cs @@ -0,0 +1,116 @@ +// 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 +{ + public class DosMatcherTests + { + [Theory, MemberData(nameof(DosMatchData)), MemberData(nameof(EscapedDosMatchData))] + public static void DosMatch(string expression, string name, bool ignoreCase, bool expected) + { + Assert.Equal(expected, FileSystemName.MatchesDosExpression(expression, name.AsReadOnlySpan(), ignoreCase)); + } + + public static TheoryData<string, string, bool, bool> EscapedDosMatchData => new TheoryData<string, string, bool, bool> + { + // Trailing escape matches as it is considered "invisible" + { "\\", "\\", false, true }, + { "\\", "\\", true, true }, + { "\\\\", "\\", false, true }, + { "\\\\", "\\", true, true }, + + + { "\\*", "a", false, false }, + { "\\*", "a", true, false }, + { "\\*", "*", false, true }, + { "\\*", "*", true, true }, + { "*\\*", "***", false, true }, + { "*\\*", "***", true, true }, + { "*\\*", "ABC*", false, true }, + { "*\\*", "ABC*", true, true }, + { "*\\*", "***A", false, false }, + { "*\\*", "***A", true, false }, + { "*\\*", "ABC*A", false, false }, + { "*\\*", "ABC*A", true, false }, + + { "\\\"", "a", false, false }, + { "\\\"", "a", true, false }, + { "\\\"", "\"", false, true }, + { "\\\"", "\"", true, true }, + }; + + public static TheoryData<string, string, bool, bool> DosMatchData => new TheoryData<string, string, bool, bool> + { + { null, "", false, false }, + { null, "", true, false }, + { "*", "", false, false }, + { "*", "", true, false }, + { "*", "ab", false, true }, + { "*", "AB", true, true }, + { "*foo", "foo", false, true }, + { "*foo", "foo", true, true }, + { "*foo", "FOO", false, false }, + { "*foo", "FOO", true, true }, + { "*foo", "nofoo", true, true }, + { "*foo", "NoFOO", true, true }, + { "*foo", "noFOO", false, false }, + + { @"*", @"foo.txt", true, true }, + { @".", @"foo.txt", true, false }, + { @".", @"footxt", true, false }, + { @"*.*", @"foo.txt", true, true }, + { @"*.*", @"foo.", true, true }, + { @"*.*", @".foo", true, true }, + { @"*.*", @"footxt", true, false }, + { "<\"*", @"footxt", true, true }, // DOS equivalent of *.* + { "<\"*", @"foo.txt", true, true }, // DOS equivalent of *.* + { "<\"*", @".foo", true, true }, // DOS equivalent of *.* + { "<\"*", @"foo.", true, true }, // DOS equivalent of *.* + { ">\">", @"a.b", true, true }, // DOS equivalent of ?.? + { ">\">", @"a.", true, true }, // DOS equivalent of ?.? + { ">\">", @"a", true, true }, // DOS equivalent of ?.? + { ">\">", @"ab", true, false }, // DOS equivalent of ?.? + { ">\">", @"a.bc", true, false }, // DOS equivalent of ?.? + { ">\">", @"ab.c", true, false }, // DOS equivalent of ?.? + { ">>\">>", @"a.b", true, true }, // DOS equivalent of ??.?? + { ">>\"\">>", @"a.b", true, false }, // Not possible to do from DOS ??""?? + { ">>\">>", @"a.bc", true, true }, // DOS equivalent of ??.?? + { ">>\">>", @"ab.ba", true, true }, // DOS equivalent of ??.?? + { ">>\">>", @"ab.", true, true }, // DOS equivalent of ??.?? + { ">>\"\"\">>", @"ab.", true, true }, // Not possible to do from DOS ??"""?? + { ">>b\">>", @"ab.ba", true, false }, // DOS equivalent of ??b.?? + { "a>>\">>", @"ab.ba", true, true }, // DOS equivalent of a??.?? + { ">>\">>a", @"ab.ba", true, false }, // DOS equivalent of ??.??a + { ">>\"b>>", @"ab.ba", true, true }, // DOS equivalent of ??.b?? + { ">>\"b>>", @"ab.b", true, true }, // DOS equivalent of ??.b?? + { ">>b.>>", @"ab.ba", true, false }, + { "a>>.>>", @"ab.ba", true, true }, + { ">>.>>a", @"ab.ba", true, false }, + { ">>.b>>", @"ab.ba", true, true }, + { ">>.b>>", @"ab.b", true, true }, + { ">>\">>\">>", @"ab.ba", true, true }, // DOS equivalent of ??.??.?? (The last " is an optional period) + { ">>\">>\">>", @"abba", true, false }, // DOS equivalent of ??.??.?? (The first " isn't, so this doesn't match) + { ">>\"ab\"ba", @"ab.ba", true, false }, // DOS equivalent of ??.ab.ba + { "ab\"ba\">>", @"ab.ba", true, true }, // DOS equivalent of ab.ba.?? + { "ab\">>\"ba", @"ab.ba", true, false }, // DOS equivalent of ab.??.ba + { ">>\">>\">>>", @"ab.ba.cab", true, true }, // DOS equivalent of ??.??.??? + { "a>>\"b>>\"c>>>", @"ab.ba.cab", true, true }, // DOS equivalent of a??.b??.c??? + { @"<", @"a", true, true }, // DOS equivalent of *. + { @"<", @"a.", true, true }, // DOS equivalent of *. + { @"<", @"a. ", true, false }, // DOS equivalent of *. + { @"<", @"a.b", true, false }, // DOS equivalent of *. + { @"foo<", @"foo.", true, true }, // DOS equivalent of foo*. + { @"foo<", @"foo. ", true, false }, // DOS equivalent of foo*. + { @"<<", @"a.b", true, true }, + { @"<<", @"a.b.c", true, true }, + { "<\"", @"a.b.c", true, false }, + { @"<.", @"a", true, false }, + { @"<.", @"a.", true, true }, + { @"<.", @"a.b", true, false }, + }; + } +} diff --git a/src/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.netcoreapp.cs b/src/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.netcoreapp.cs new file mode 100644 index 0000000000..7aa609b452 --- /dev/null +++ b/src/System.IO.FileSystem/tests/Enumeration/SkipAttributeTests.netcoreapp.cs @@ -0,0 +1,85 @@ +// 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 System.Linq; +using Xunit; + +namespace System.IO.Tests.Enumeration +{ + public class SkipAttributeTests : FileSystemTest + { + protected virtual string[] GetPaths(string directory, EnumerationOptions options) + { + return new FileSystemEnumerable<string>( + directory, + (ref FileSystemEntry entry) => entry.ToFullPath(), + options) + { + ShouldIncludePredicate = (ref FileSystemEntry entry) => { return !entry.IsDirectory; } + }.ToArray(); + } + + [Fact] + public void SkippingHiddenFiles() + { + DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); + DirectoryInfo testSubdirectory = Directory.CreateDirectory(Path.Combine(testDirectory.FullName, GetTestFileName())); + FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, GetTestFileName())); + + // Put a period in front to make it hidden on Unix + FileInfo fileTwo = new FileInfo(Path.Combine(testDirectory.FullName, "." + GetTestFileName())); + FileInfo fileThree = new FileInfo(Path.Combine(testSubdirectory.FullName, GetTestFileName())); + FileInfo fileFour = new FileInfo(Path.Combine(testSubdirectory.FullName, "." + GetTestFileName())); + + fileOne.Create().Dispose(); + fileTwo.Create().Dispose(); + if (PlatformDetection.IsWindows) + fileTwo.Attributes = fileTwo.Attributes | FileAttributes.Hidden; + fileThree.Create().Dispose(); + fileFour.Create().Dispose(); + if (PlatformDetection.IsWindows) + fileFour.Attributes = fileTwo.Attributes | FileAttributes.Hidden; + + string[] paths = GetPaths(testDirectory.FullName, new EnumerationOptions { AttributesToSkip = FileAttributes.Hidden }); + Assert.Equal(new string[] { fileOne.FullName }, paths); + + paths = GetPaths(testDirectory.FullName, new EnumerationOptions { AttributesToSkip = FileAttributes.Hidden, RecurseSubdirectories = true }); + Assert.Equal(new string[] { fileOne.FullName, fileThree.FullName }, paths); + + if (PlatformDetection.IsWindows) + { + // Shouldn't recurse into the subdirectory now that it is hidden + testSubdirectory.Attributes = testSubdirectory.Attributes | FileAttributes.Hidden; + } + else + { + Directory.Move(testSubdirectory.FullName, Path.Combine(testDirectory.FullName, "." + testSubdirectory.Name)); + } + + paths = GetPaths(testDirectory.FullName, new EnumerationOptions { AttributesToSkip = FileAttributes.Hidden, RecurseSubdirectories = true }); + Assert.Equal(new string[] { fileOne.FullName }, paths); + } + } + + // Unix implementation not finished + [ActiveIssue(26715, TestPlatforms.AnyUnix)] + public class SkipAttributeTests_Directory_GetFiles : SkipAttributeTests + { + protected override string[] GetPaths(string directory, EnumerationOptions options) + { + return Directory.GetFiles(directory, "*", options); + } + } + + // Unix implementation not finished + [ActiveIssue(26715, TestPlatforms.AnyUnix)] + public class SkipAttributeTests_DirectoryInfo_GetFiles : SkipAttributeTests + { + protected override string[] GetPaths(string directory, EnumerationOptions options) + { + return new DirectoryInfo(directory).GetFiles("*", options).Select(i => i.FullName).ToArray(); + } + } +} diff --git a/src/System.IO.FileSystem/tests/Enumeration/SpecialDirectoryTests.netcoreapp.cs b/src/System.IO.FileSystem/tests/Enumeration/SpecialDirectoryTests.netcoreapp.cs new file mode 100644 index 0000000000..98f3a2098a --- /dev/null +++ b/src/System.IO.FileSystem/tests/Enumeration/SpecialDirectoryTests.netcoreapp.cs @@ -0,0 +1,37 @@ +// 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 System.Linq; +using Xunit; + +namespace System.IO.Tests.Enumeration +{ + public class SpecialDirectoryTests : FileSystemTest + { + protected virtual string[] GetNames(string directory, EnumerationOptions options) + { + return new FileSystemEnumerable<string>( + directory, + (ref FileSystemEntry entry) => new string(entry.FileName), + options).ToArray(); + } + + [Fact] + public void SkippingHiddenFiles() + { + string[] paths = GetNames(TestDirectory, new EnumerationOptions { ReturnSpecialDirectories = true }); + Assert.Contains(".", paths); + Assert.Contains("..", paths); + } + } + + public class SpecialDirectoryTests_DirectoryInfo_GetDirectories : SpecialDirectoryTests + { + protected override string[] GetNames(string directory, EnumerationOptions options) + { + return new DirectoryInfo(directory).GetDirectories("*", options).Select(i => i.Name).ToArray(); + } + } +} 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 b6637446b0..c4ed74a5e8 100644 --- a/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -53,6 +53,10 @@ <Compile Include="File\ReadWriteAllBytesAsync.cs" /> <Compile Include="File\ReadWriteAllTextAsync.cs" /> <Compile Include="FileStream\ReadWriteSpan.netcoreapp.cs" /> + <Compile Include="Enumeration\ConstructionTests.netcoreapp.cs" /> + <Compile Include="Enumeration\SpecialDirectoryTests.netcoreapp.cs" /> + <Compile Include="Enumeration\SkipAttributeTests.netcoreapp.cs" /> + <Compile Include="Enumeration\DosMatcherTests.netcoreapp.cs" /> </ItemGroup> <ItemGroup> <!-- Rewritten --> @@ -181,5 +185,8 @@ <ItemGroup> <EmbeddedResource Include="Resources\$(AssemblyName).rd.xml" /> </ItemGroup> + <ItemGroup> + <Folder Include="Matchers\" /> + </ItemGroup> <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" /> </Project>
\ No newline at end of file |