Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mono/corefx.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy Kuhne <jeremy.kuhne@microsoft.com>2017-12-01 02:46:56 +0300
committerGitHub <noreply@github.com>2017-12-01 02:46:56 +0300
commiteb0d4388c339288a589bdd7f5e0c9fcd61da4362 (patch)
treea18400519f183a3669481dc9cc234aeb158f6c27
parent991fb251e3290dc50eda58343a1c9bcd6bbe03e5 (diff)
Fast file enumeration for Windows (#25426)
* Fast file enumeration for Windows Implements an optimized, low allocation replacement for file enumeration on Windows. In preliminary tests this improves performance at least two fold, particularly for recursive searches. Removes aggressive filter validation. Copies and cleans up filename matching code from FileSystemWatcher. Add length setter for ValueStringBuilder. Remove Unix .. check for enumerable.
-rw-r--r--src/Common/src/Interop/Windows/Interop.BOOLEAN.cs21
-rw-r--r--src/Common/src/Interop/Windows/Interop.LongFileTime.cs28
-rw-r--r--src/Common/src/Interop/Windows/NtDll/Interop.FILE_FULL_DIR_INFORMATION.cs73
-rw-r--r--src/Common/src/Interop/Windows/NtDll/Interop.FILE_INFORMATION_CLASS.cs85
-rw-r--r--src/Common/src/Interop/Windows/NtDll/Interop.IO_STATUS_BLOCK.cs47
-rw-r--r--src/Common/src/Interop/Windows/NtDll/Interop.NtQueryDirectoryFile.cs28
-rw-r--r--src/Common/src/Interop/Windows/NtDll/Interop.NtStatus.cs1
-rw-r--r--src/Common/src/Interop/Windows/NtDll/Interop.RtlNtStatusToDosError.cs17
-rw-r--r--src/Common/src/Interop/Windows/kernel32/Interop.CompareStringOrdinal.cs20
-rw-r--r--src/Common/src/Interop/Windows/kernel32/Interop.CreateFile.cs33
-rw-r--r--src/Common/src/Interop/Windows/kernel32/Interop.CreateFile2.cs55
-rw-r--r--src/Common/src/Interop/Windows/kernel32/Interop.FILE_INFO_BY_HANDLE_CLASS.cs153
-rw-r--r--src/Common/src/Interop/Windows/kernel32/Interop.GetFileInformationByHandleEx.cs20
-rw-r--r--src/Common/src/Interop/Windows/kernel32/Interop.SetFileInformationByHandle.cs29
-rw-r--r--src/Common/src/System/IO/PathInternal.Unix.cs12
-rw-r--r--src/Common/src/System/Text/ValueStringBuilder.cs17
-rw-r--r--src/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs28
-rw-r--r--src/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj5
-rw-r--r--src/System.IO.FileSystem/src/System.IO.FileSystem.csproj58
-rw-r--r--src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.Unix.cs37
-rw-r--r--src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.Windows.cs44
-rw-r--r--src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.cs24
-rw-r--r--src/System.IO.FileSystem/src/System/IO/DirectoryInfo.Windows.cs9
-rw-r--r--src/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs83
-rw-r--r--src/System.IO.FileSystem/src/System/IO/DosMatcher.cs315
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FileInfo.Windows.cs9
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FileInfo.cs44
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs10
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FindEnumerable.Win32.cs43
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FindEnumerable.WinRT.cs35
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FindEnumerable.Windows.cs251
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FindEnumerableFactory.cs138
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FindPredicate.cs11
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FindPredicates.cs18
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FindTransform.cs11
-rw-r--r--src/System.IO.FileSystem/src/System/IO/FindTransforms.cs40
-rw-r--r--src/System.IO.FileSystem/src/System/IO/PathHelpers.Unix.cs15
-rw-r--r--src/System.IO.FileSystem/src/System/IO/PathHelpers.Windows.cs83
-rw-r--r--src/System.IO.FileSystem/src/System/IO/PathHelpers.cs149
-rw-r--r--src/System.IO.FileSystem/src/System/IO/RawFindData.cs33
-rw-r--r--src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs1
-rw-r--r--src/System.IO.FileSystem/src/System/IO/Win32FileSystem.cs23
-rw-r--r--src/System.IO.FileSystem/src/System/IO/Win32FileSystemEnumerable.cs637
-rw-r--r--src/System.IO.FileSystem/tests/Directory/EnumerableTests.cs80
-rw-r--r--src/System.IO.FileSystem/tests/Directory/GetFileSystemEntries_str_str.cs215
-rw-r--r--src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj1
-rw-r--r--src/System.IO.IsolatedStorage/tests/System/IO/IsolatedStorage/GetFileNamesTests.cs15
-rw-r--r--src/System.IO.Ports/src/System.IO.Ports.csproj5
48 files changed, 2209 insertions, 900 deletions
diff --git a/src/Common/src/Interop/Windows/Interop.BOOLEAN.cs b/src/Common/src/Interop/Windows/Interop.BOOLEAN.cs
new file mode 100644
index 0000000000..4874734534
--- /dev/null
+++ b/src/Common/src/Interop/Windows/Interop.BOOLEAN.cs
@@ -0,0 +1,21 @@
+// 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.
+
+internal partial class Interop
+{
+ /// <summary>
+ /// Blittable version of Windows BOOLEAN type. It is convenient in situations where
+ /// manual marshalling is required, or to avoid overhead of regular bool marshalling.
+ /// </summary>
+ /// <remarks>
+ /// Some Windows APIs return arbitrary integer values although the return type is defined
+ /// as BOOLEAN. It is best to never compare BOOLEAN to TRUE. Always use bResult != BOOLEAN.FALSE
+ /// or bResult == BOOLEAN.FALSE .
+ /// </remarks>
+ internal enum BOOLEAN : byte
+ {
+ FALSE = 0,
+ TRUE = 1,
+ }
+}
diff --git a/src/Common/src/Interop/Windows/Interop.LongFileTime.cs b/src/Common/src/Interop/Windows/Interop.LongFileTime.cs
new file mode 100644
index 0000000000..411ae9e4c8
--- /dev/null
+++ b/src/Common/src/Interop/Windows/Interop.LongFileTime.cs
@@ -0,0 +1,28 @@
+// 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;
+
+internal partial class Interop
+{
+ /// <summary>
+ /// 100-nanosecond intervals (ticks) since January 1, 1601 (UTC).
+ /// </summary>
+ /// <remarks>
+ /// For NT times that are defined as longs (LARGE_INTEGER, etc.).
+ /// Do NOT use for FILETIME unless you are POSITIVE it will fall on an
+ /// 8 byte boundary.
+ /// </remarks>
+ internal struct LongFileTime
+ {
+#pragma warning disable CS0649
+ /// <summary>
+ /// 100-nanosecond intervals (ticks) since January 1, 1601 (UTC).
+ /// </summary>
+ internal long TicksSince1601;
+#pragma warning restore CS0649
+
+ internal DateTime ToDateTimeUtc() => DateTime.FromFileTimeUtc(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
new file mode 100644
index 0000000000..a1037696f6
--- /dev/null
+++ b/src/Common/src/Interop/Windows/NtDll/Interop.FILE_FULL_DIR_INFORMATION.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+internal partial class Interop
+{
+ internal partial class NtDll
+ {
+ /// <summary>
+ /// <a href="https://msdn.microsoft.com/en-us/library/windows/hardware/ff540289.aspx">FILE_FULL_DIR_INFORMATION</a> structure.
+ /// Used with GetFileInformationByHandleEx and FileIdBothDirectoryInfo/RestartInfo as well as NtQueryFileInformation.
+ /// Equivalent to <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/hh447298.aspx">FILE_FULL_DIR_INFO</a> structure.
+ /// </summary>
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public unsafe struct FILE_FULL_DIR_INFORMATION
+ {
+ /// <summary>
+ /// Offset in bytes of the next entry, if any.
+ /// </summary>
+ public uint NextEntryOffset;
+
+ /// <summary>
+ /// Byte offset within the parent directory, undefined for NTFS.
+ /// </summary>
+ public uint FileIndex;
+ public LongFileTime CreationTime;
+ public LongFileTime LastAccessTime;
+ public LongFileTime LastWriteTime;
+ public LongFileTime ChangeTime;
+ public long EndOfFile;
+ public long AllocationSize;
+
+ /// <summary>
+ /// File attributes.
+ /// </summary>
+ /// <remarks>
+ /// Note that MSDN documentation isn't correct for this- it can return
+ /// any FILE_ATTRIBUTE that is currently set on the file, not just the
+ /// ones documented.
+ /// </remarks>
+ public FileAttributes FileAttributes;
+
+ /// <summary>
+ /// The length of the file name in bytes (without null).
+ /// </summary>
+ public uint FileNameLength;
+
+ /// <summary>
+ /// The extended attribute size OR the reparse tag if a reparse point.
+ /// </summary>
+ public uint EaSize;
+
+ private char _fileName;
+ public ReadOnlySpan<char> FileName { get { fixed (char* c = &_fileName) { return new ReadOnlySpan<char>(c, (int)FileNameLength / sizeof(char)); } } }
+
+ /// <summary>
+ /// Gets the next info pointer or null if there are no more.
+ /// </summary>
+ public unsafe static FILE_FULL_DIR_INFORMATION* GetNextInfo(FILE_FULL_DIR_INFORMATION* info)
+ {
+ uint nextOffset = (*info).NextEntryOffset;
+ if (nextOffset == 0)
+ return null;
+
+ return (FILE_FULL_DIR_INFORMATION*)((byte*)info + nextOffset);
+ }
+ }
+ }
+}
diff --git a/src/Common/src/Interop/Windows/NtDll/Interop.FILE_INFORMATION_CLASS.cs b/src/Common/src/Interop/Windows/NtDll/Interop.FILE_INFORMATION_CLASS.cs
new file mode 100644
index 0000000000..ec856e61af
--- /dev/null
+++ b/src/Common/src/Interop/Windows/NtDll/Interop.FILE_INFORMATION_CLASS.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;
+using System.Runtime.InteropServices;
+
+internal partial class Interop
+{
+ internal partial class NtDll
+ {
+ // https://msdn.microsoft.com/en-us/library/windows/hardware/ff728840.aspx
+ public enum FILE_INFORMATION_CLASS : uint
+ {
+ FileDirectoryInformation = 1,
+ FileFullDirectoryInformation = 2,
+ FileBothDirectoryInformation = 3,
+ FileBasicInformation = 4,
+ FileStandardInformation = 5,
+ FileInternalInformation = 6,
+ FileEaInformation = 7,
+ FileAccessInformation = 8,
+ FileNameInformation = 9,
+ FileRenameInformation = 10,
+ FileLinkInformation = 11,
+ FileNamesInformation = 12,
+ FileDispositionInformation = 13,
+ FilePositionInformation = 14,
+ FileFullEaInformation = 15,
+ FileModeInformation = 16,
+ FileAlignmentInformation = 17,
+ FileAllInformation = 18,
+ FileAllocationInformation = 19,
+ FileEndOfFileInformation = 20,
+ FileAlternateNameInformation = 21,
+ FileStreamInformation = 22,
+ FilePipeInformation = 23,
+ FilePipeLocalInformation = 24,
+ FilePipeRemoteInformation = 25,
+ FileMailslotQueryInformation = 26,
+ FileMailslotSetInformation = 27,
+ FileCompressionInformation = 28,
+ FileObjectIdInformation = 29,
+ FileCompletionInformation = 30,
+ FileMoveClusterInformation = 31,
+ FileQuotaInformation = 32,
+ FileReparsePointInformation = 33,
+ FileNetworkOpenInformation = 34,
+ FileAttributeTagInformation = 35,
+ FileTrackingInformation = 36,
+ FileIdBothDirectoryInformation = 37,
+ FileIdFullDirectoryInformation = 38,
+ FileValidDataLengthInformation = 39,
+ FileShortNameInformation = 40,
+ FileIoCompletionNotificationInformation = 41,
+ FileIoStatusBlockRangeInformation = 42,
+ FileIoPriorityHintInformation = 43,
+ FileSfioReserveInformation = 44,
+ FileSfioVolumeInformation = 45,
+ FileHardLinkInformation = 46,
+ FileProcessIdsUsingFileInformation = 47,
+ FileNormalizedNameInformation = 48,
+ FileNetworkPhysicalNameInformation = 49,
+ FileIdGlobalTxDirectoryInformation = 50,
+ FileIsRemoteDeviceInformation = 51,
+ FileUnusedInformation = 52,
+ FileNumaNodeInformation = 53,
+ FileStandardLinkInformation = 54,
+ FileRemoteProtocolInformation = 55,
+ FileRenameInformationBypassAccessCheck = 56,
+ FileLinkInformationBypassAccessCheck = 57,
+ FileVolumeNameInformation = 58,
+ FileIdInformation = 59,
+ FileIdExtdDirectoryInformation = 60,
+ FileReplaceCompletionInformation = 61,
+ FileHardLinkFullIdInformation = 62,
+ FileIdExtdBothDirectoryInformation = 63,
+ FileDispositionInformationEx = 64,
+ FileRenameInformationEx = 65,
+ FileRenameInformationExBypassAccessCheck = 66,
+ FileDesiredStorageClassInformation = 67,
+ FileStatInformation = 68
+ }
+ }
+}
diff --git a/src/Common/src/Interop/Windows/NtDll/Interop.IO_STATUS_BLOCK.cs b/src/Common/src/Interop/Windows/NtDll/Interop.IO_STATUS_BLOCK.cs
new file mode 100644
index 0000000000..f372a56c60
--- /dev/null
+++ b/src/Common/src/Interop/Windows/NtDll/Interop.IO_STATUS_BLOCK.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+
+internal partial class Interop
+{
+ internal partial class NtDll
+ {
+ // https://msdn.microsoft.com/en-us/library/windows/hardware/ff550671.aspx
+ [StructLayout(LayoutKind.Sequential)]
+ public struct IO_STATUS_BLOCK
+ {
+ /// <summary>
+ /// Status
+ /// </summary>
+ public IO_STATUS Status;
+
+ /// <summary>
+ /// Request dependent value.
+ /// </summary>
+ public IntPtr Information;
+
+ // This isn't an actual Windows type, it is a union within IO_STATUS_BLOCK. We *have* to separate it out as
+ // the size of IntPtr varies by architecture and we can't specify the size at compile time to offset the
+ // Information pointer in the status block.
+ [StructLayout(LayoutKind.Explicit)]
+ public struct IO_STATUS
+ {
+ /// <summary>
+ /// The completion status, either STATUS_SUCCESS if the operation was completed successfully or
+ /// some other informational, warning, or error status.
+ /// </summary>
+ [FieldOffset(0)]
+ public uint Status;
+
+ /// <summary>
+ /// Reserved for internal use.
+ /// </summary>
+ [FieldOffset(0)]
+ public IntPtr Pointer;
+ }
+ }
+ }
+}
diff --git a/src/Common/src/Interop/Windows/NtDll/Interop.NtQueryDirectoryFile.cs b/src/Common/src/Interop/Windows/NtDll/Interop.NtQueryDirectoryFile.cs
new file mode 100644
index 0000000000..e648c0e87d
--- /dev/null
+++ b/src/Common/src/Interop/Windows/NtDll/Interop.NtQueryDirectoryFile.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+
+internal partial class Interop
+{
+ internal partial class NtDll
+ {
+ // https://msdn.microsoft.com/en-us/library/windows/hardware/ff556633.aspx
+ // https://msdn.microsoft.com/en-us/library/windows/hardware/ff567047.aspx
+ [DllImport(Libraries.NtDll, CharSet = CharSet.Unicode, ExactSpelling = true)]
+ public unsafe static extern int NtQueryDirectoryFile(
+ IntPtr FileHandle,
+ IntPtr Event,
+ IntPtr ApcRoutine,
+ IntPtr ApcContext,
+ out IO_STATUS_BLOCK IoStatusBlock,
+ byte[] FileInformation,
+ uint Length,
+ FILE_INFORMATION_CLASS FileInformationClass,
+ BOOLEAN ReturnSingleEntry,
+ UNICODE_STRING* FileName,
+ BOOLEAN RestartScan);
+ }
+}
diff --git a/src/Common/src/Interop/Windows/NtDll/Interop.NtStatus.cs b/src/Common/src/Interop/Windows/NtDll/Interop.NtStatus.cs
index 232d0ea216..2d51c44f54 100644
--- a/src/Common/src/Interop/Windows/NtDll/Interop.NtStatus.cs
+++ b/src/Common/src/Interop/Windows/NtDll/Interop.NtStatus.cs
@@ -9,6 +9,7 @@ internal static partial class Interop
// Error codes from ntstatus.h
internal const uint STATUS_SUCCESS = 0x00000000;
internal const uint STATUS_SOME_NOT_MAPPED = 0x00000107;
+ internal const uint STATUS_NO_MORE_FILES = 0x80000006;
internal const uint STATUS_INVALID_PARAMETER = 0xC000000D;
internal const uint STATUS_NO_MEMORY = 0xC0000017;
internal const uint STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034;
diff --git a/src/Common/src/Interop/Windows/NtDll/Interop.RtlNtStatusToDosError.cs b/src/Common/src/Interop/Windows/NtDll/Interop.RtlNtStatusToDosError.cs
new file mode 100644
index 0000000000..05e8ce4cf4
--- /dev/null
+++ b/src/Common/src/Interop/Windows/NtDll/Interop.RtlNtStatusToDosError.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+
+internal partial class Interop
+{
+ internal partial class NtDll
+ {
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ms680600(v=vs.85).aspx
+ [DllImport(Libraries.NtDll, ExactSpelling = true)]
+ public unsafe static extern uint RtlNtStatusToDosError(
+ int Status);
+ }
+}
diff --git a/src/Common/src/Interop/Windows/kernel32/Interop.CompareStringOrdinal.cs b/src/Common/src/Interop/Windows/kernel32/Interop.CompareStringOrdinal.cs
new file mode 100644
index 0000000000..b0cef5dafd
--- /dev/null
+++ b/src/Common/src/Interop/Windows/kernel32/Interop.CompareStringOrdinal.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.
+
+using System.Runtime.InteropServices;
+
+internal partial class Interop
+{
+ internal partial class Kernel32
+ {
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd317762.aspx
+ [DllImport(Libraries.Kernel32, CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
+ public unsafe static extern int CompareStringOrdinal(
+ ref char lpString1,
+ int cchCount1,
+ ref char lpString2,
+ int cchCount2,
+ bool bIgnoreCase);
+ }
+}
diff --git a/src/Common/src/Interop/Windows/kernel32/Interop.CreateFile.cs b/src/Common/src/Interop/Windows/kernel32/Interop.CreateFile.cs
index 8451e8fb95..9c3639d6e2 100644
--- a/src/Common/src/Interop/Windows/kernel32/Interop.CreateFile.cs
+++ b/src/Common/src/Interop/Windows/kernel32/Interop.CreateFile.cs
@@ -11,11 +11,12 @@ internal partial class Interop
{
internal partial class Kernel32
{
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858.aspx
/// <summary>
/// WARNING: The private methods do not implicitly handle long paths. Use CreateFile.
/// </summary>
[DllImport(Libraries.Kernel32, EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false, ExactSpelling = true)]
- private unsafe static extern SafeFileHandle CreateFilePrivate(
+ private unsafe static extern IntPtr CreateFilePrivate(
string lpFileName,
int dwDesiredAccess,
FileShare dwShareMode,
@@ -36,7 +37,16 @@ internal partial class Interop
lpFileName = PathInternal.EnsureExtendedPrefixOverMaxPath(lpFileName);
fixed (SECURITY_ATTRIBUTES* sa = &securityAttrs)
{
- return CreateFilePrivate(lpFileName, dwDesiredAccess, dwShareMode, sa, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
+ IntPtr handle = CreateFilePrivate(lpFileName, dwDesiredAccess, dwShareMode, sa, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
+ try
+ {
+ return new SafeFileHandle(handle, ownsHandle: true);
+ }
+ catch
+ {
+ CloseHandle(handle);
+ throw;
+ }
}
}
@@ -47,6 +57,25 @@ internal partial class Interop
FileMode dwCreationDisposition,
int dwFlagsAndAttributes)
{
+ IntPtr handle = CreateFile_IntPtr(lpFileName, dwDesiredAccess, dwShareMode, dwCreationDisposition, dwFlagsAndAttributes);
+ try
+ {
+ return new SafeFileHandle(handle, ownsHandle: true);
+ }
+ catch
+ {
+ CloseHandle(handle);
+ throw;
+ }
+ }
+
+ internal unsafe static IntPtr CreateFile_IntPtr(
+ string lpFileName,
+ int dwDesiredAccess,
+ FileShare dwShareMode,
+ FileMode dwCreationDisposition,
+ int dwFlagsAndAttributes)
+ {
lpFileName = PathInternal.EnsureExtendedPrefixOverMaxPath(lpFileName);
return CreateFilePrivate(lpFileName, dwDesiredAccess, dwShareMode, null, dwCreationDisposition, dwFlagsAndAttributes, IntPtr.Zero);
}
diff --git a/src/Common/src/Interop/Windows/kernel32/Interop.CreateFile2.cs b/src/Common/src/Interop/Windows/kernel32/Interop.CreateFile2.cs
index 830e51f13e..ed691ace46 100644
--- a/src/Common/src/Interop/Windows/kernel32/Interop.CreateFile2.cs
+++ b/src/Common/src/Interop/Windows/kernel32/Interop.CreateFile2.cs
@@ -4,26 +4,28 @@
using Microsoft.Win32.SafeHandles;
using System;
+using System.IO;
using System.Runtime.InteropServices;
internal partial class Interop
{
internal partial class Kernel32
{
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/hh449422.aspx
[DllImport(Libraries.Kernel32, EntryPoint = "CreateFile2", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
- internal unsafe static extern SafeFileHandle CreateFile2(
+ internal unsafe static extern IntPtr CreateFile2(
string lpFileName,
int dwDesiredAccess,
- System.IO.FileShare dwShareMode,
- System.IO.FileMode dwCreationDisposition,
+ FileShare dwShareMode,
+ FileMode dwCreationDisposition,
[In] ref CREATEFILE2_EXTENDED_PARAMETERS parameters);
- private static unsafe SafeFileHandle CreateFile(
+ private static unsafe IntPtr CreateFile(
string lpFileName,
int dwDesiredAccess,
- System.IO.FileShare dwShareMode,
+ FileShare dwShareMode,
SECURITY_ATTRIBUTES* securityAttrs,
- System.IO.FileMode dwCreationDisposition,
+ FileMode dwCreationDisposition,
int dwFlagsAndAttributes,
IntPtr hTemplateFile)
{
@@ -36,32 +38,61 @@ internal partial class Interop
parameters.hTemplateFile = hTemplateFile;
parameters.lpSecurityAttributes = securityAttrs;
-
+
return CreateFile2(lpFileName, dwDesiredAccess, dwShareMode, dwCreationDisposition, ref parameters);
}
internal static unsafe SafeFileHandle CreateFile(
string lpFileName,
int dwDesiredAccess,
- System.IO.FileShare dwShareMode,
+ FileShare dwShareMode,
ref SECURITY_ATTRIBUTES securityAttrs,
- System.IO.FileMode dwCreationDisposition,
+ FileMode dwCreationDisposition,
int dwFlagsAndAttributes,
IntPtr hTemplateFile)
{
fixed (SECURITY_ATTRIBUTES* lpSecurityAttributes = &securityAttrs)
{
- return CreateFile(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
+ IntPtr handle = CreateFile(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
+ try
+ {
+ return new SafeFileHandle(handle, ownsHandle: true);
+ }
+ catch
+ {
+ CloseHandle(handle);
+ throw;
+ }
}
}
internal static unsafe SafeFileHandle CreateFile(
string lpFileName,
int dwDesiredAccess,
- System.IO.FileShare dwShareMode,
- System.IO.FileMode dwCreationDisposition,
+ FileShare dwShareMode,
+ FileMode dwCreationDisposition,
+ int dwFlagsAndAttributes)
+ {
+ IntPtr handle = CreateFile(lpFileName, dwDesiredAccess, dwShareMode, null, dwCreationDisposition, dwFlagsAndAttributes, IntPtr.Zero);
+ try
+ {
+ return new SafeFileHandle(handle, ownsHandle: true);
+ }
+ catch
+ {
+ CloseHandle(handle);
+ throw;
+ }
+ }
+
+ internal unsafe static IntPtr CreateFile_IntPtr(
+ string lpFileName,
+ int dwDesiredAccess,
+ FileShare dwShareMode,
+ FileMode dwCreationDisposition,
int dwFlagsAndAttributes)
{
+ lpFileName = PathInternal.EnsureExtendedPrefixOverMaxPath(lpFileName);
return CreateFile(lpFileName, dwDesiredAccess, dwShareMode, null, dwCreationDisposition, dwFlagsAndAttributes, IntPtr.Zero);
}
}
diff --git a/src/Common/src/Interop/Windows/kernel32/Interop.FILE_INFO_BY_HANDLE_CLASS.cs b/src/Common/src/Interop/Windows/kernel32/Interop.FILE_INFO_BY_HANDLE_CLASS.cs
new file mode 100644
index 0000000000..53ed7484b0
--- /dev/null
+++ b/src/Common/src/Interop/Windows/kernel32/Interop.FILE_INFO_BY_HANDLE_CLASS.cs
@@ -0,0 +1,153 @@
+// 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.
+
+internal partial class Interop
+{
+ internal partial class Kernel32
+ {
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/aa364228.aspx
+ internal enum FILE_INFO_BY_HANDLE_CLASS : uint
+ {
+ // Up to FileRemoteProtocolInfo available in Windows 7
+
+ /// <summary>
+ /// Returns basic information (timestamps and attributes).
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryInformationFile and FileBasicInformation.
+ /// </remarks>
+ FileBasicInfo,
+
+ /// <summary>
+ /// Returns file size, link count, pending delete status, and if it is a directory.
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryInformationFile and FileStandardInformation.
+ /// </remarks>
+ FileStandardInfo,
+
+ /// <summary>
+ /// Gets the file name.
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryInformationFile and FileNameInformation.
+ /// </remarks>
+ FileNameInfo,
+
+ /// <summary>
+ /// Renames a file. Allows renaming a file without having to specify a full path, if you have
+ /// a handle to the directory the file resides in.
+ /// </summary>
+ /// <remarks>
+ /// Only valid for SetFileInformationByHandle. Thunks to NtSetInformationFile and FileRenameInformation.
+ /// MoveFileEx is effectively the same API.
+ /// </remarks>
+ FileRenameInfo,
+
+ /// <summary>
+ /// Allows marking a file handle for deletion. Handle must have been opened with Delete access.
+ /// You cannot change the state of a handle opened with DeleteOnClose.
+ /// </summary>
+ /// <remarks>
+ /// Only valid for SetFileInformationByHandle. Thunks to NtSetInformationFile and FileDispositionInformation.
+ /// DeleteFile is effectively the same API.
+ /// </remarks>
+ FileDispositionInfo,
+
+ /// <summary>
+ /// Allows setting the allocated size of the file.
+ /// </summary>
+ /// <remarks>
+ /// Only valid for SetFileInformationByHandle. Thunks to NtSetInformationFile.
+ /// SetEndOfFile sets this after setting the logical end of file to the current position via FileEndOfFileInfo.
+ /// </remarks>
+ FileAllocationInfo,
+
+ /// <summary>
+ /// Allows setting the end of file.
+ /// </summary>
+ /// <remarks>
+ /// Only valid for SetFileInformationByHandle. Thunks to NtSetInformationFile.
+ /// SetEndOfFile calls this to set the logical end of file to whatever the current position is.
+ /// </remarks>
+ FileEndOfFileInfo,
+
+ /// <summary>
+ /// Gets stream information for the file.
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryInformationFile and FileStreamInformation.
+ /// </remarks>
+ FileStreamInfo,
+
+ /// <summary>
+ /// Gets compression information for the file.
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryInformationFile and FileCompressionInformation.
+ /// </remarks>
+ FileCompressionInfo,
+
+ /// <summary>
+ /// Gets the file attributes and reparse tag.
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryInformationFile and FileAttributeTagInformation.
+ /// </remarks>
+ FileAttributeTagInfo,
+
+ /// <summary>
+ /// Starts a query for for file information in a directory.
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryDirectoryFile and FileIdBothDirectoryInformation with RestartScan
+ /// set to false.
+ /// </remarks>
+ FileIdBothDirectoryInfo,
+
+ /// <summary>
+ /// Resumes a query for file information in a directory.
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryDirectoryFile and FileIdBothDirectoryInformation with RestartScan
+ /// set to true.
+ /// </remarks>
+ FileIdBothDirectoryRestartInfo,
+
+ /// <summary>
+ /// Allows setting the priority hint for a file.
+ /// </summary>
+ /// <remarks>
+ /// Only valid for SetFileInformationByHandle. Thunks to NtSetInformationFile.
+ /// </remarks>
+ FileIoPriorityHintInfo,
+
+ /// <summary>
+ /// Gets the file remote protocol information.
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryInformationFile and FileRemoteProtocolInformation.
+ /// </remarks>
+ FileRemoteProtocolInfo,
+
+ /// <summary>
+ /// Starts a query for for file information in a directory. Uses FILE_FULL_DIR_INFO.
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryDirectoryFile and FileFullDirectoryInformation with RestartScan
+ /// set to false. Windows 8 and up.
+ /// </remarks>
+ FileFullDirectoryInfo,
+
+ /// <summary>
+ /// Resumes a query for file information in a directory. Uses FILE_FULL_DIR_INFO.
+ /// </summary>
+ /// <remarks>
+ /// Thunks to NtQueryDirectoryFile and FileFullDirectoryInformation with RestartScan
+ /// set to true. Windows 8 and up.
+ /// </remarks>
+ FileFullDirectoryRestartInfo
+ }
+ }
+}
diff --git a/src/Common/src/Interop/Windows/kernel32/Interop.GetFileInformationByHandleEx.cs b/src/Common/src/Interop/Windows/kernel32/Interop.GetFileInformationByHandleEx.cs
new file mode 100644
index 0000000000..b41d261703
--- /dev/null
+++ b/src/Common/src/Interop/Windows/kernel32/Interop.GetFileInformationByHandleEx.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.
+
+using System;
+using System.Runtime.InteropServices;
+
+internal partial class Interop
+{
+ internal partial class Kernel32
+ {
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/aa364953.aspx
+ [DllImport(Libraries.Kernel32, SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
+ public unsafe static extern bool GetFileInformationByHandleEx(
+ IntPtr hFile,
+ FILE_INFO_BY_HANDLE_CLASS FileInformationClass,
+ byte[] lpFileInformation,
+ uint dwBufferSize);
+ }
+}
diff --git a/src/Common/src/Interop/Windows/kernel32/Interop.SetFileInformationByHandle.cs b/src/Common/src/Interop/Windows/kernel32/Interop.SetFileInformationByHandle.cs
index 0d7efe9d74..14a98a52ce 100644
--- a/src/Common/src/Interop/Windows/kernel32/Interop.SetFileInformationByHandle.cs
+++ b/src/Common/src/Interop/Windows/kernel32/Interop.SetFileInformationByHandle.cs
@@ -10,7 +10,8 @@ internal partial class Interop
{
internal partial class Kernel32
{
- [DllImport(Libraries.Kernel32, SetLastError = true)]
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365539.aspx
+ [DllImport(Libraries.Kernel32, SetLastError = true, ExactSpelling = true)]
internal static extern bool SetFileInformationByHandle(SafeFileHandle hFile, FILE_INFO_BY_HANDLE_CLASS FileInformationClass, ref FILE_BASIC_INFO lpFileInformation, uint dwBufferSize);
// Default values indicate "no change". Use defaults so that we don't force callsites to be aware of the default values
@@ -42,31 +43,5 @@ internal partial class Interop
internal long ChangeTime;
internal uint FileAttributes;
}
-
- internal enum FILE_INFO_BY_HANDLE_CLASS : uint
- {
- FileBasicInfo = 0x0u,
- FileStandardInfo = 0x1u,
- FileNameInfo = 0x2u,
- FileRenameInfo = 0x3u,
- FileDispositionInfo = 0x4u,
- FileAllocationInfo = 0x5u,
- FileEndOfFileInfo = 0x6u,
- FileStreamInfo = 0x7u,
- FileCompressionInfo = 0x8u,
- FileAttributeTagInfo = 0x9u,
- FileIdBothDirectoryInfo = 0xAu,
- FileIdBothDirectoryRestartInfo = 0xBu,
- FileIoPriorityHintInfo = 0xCu,
- FileRemoteProtocolInfo = 0xDu,
- FileFullDirectoryInfo = 0xEu,
- FileFullDirectoryRestartInfo = 0xFu,
- FileStorageInfo = 0x10u,
- FileAlignmentInfo = 0x11u,
- FileIdInfo = 0x12u,
- FileIdExtdDirectoryInfo = 0x13u,
- FileIdExtdDirectoryRestartInfo = 0x14u,
- MaximumFileInfoByHandleClass = 0x15u,
- }
}
}
diff --git a/src/Common/src/System/IO/PathInternal.Unix.cs b/src/Common/src/System/IO/PathInternal.Unix.cs
index 4a7b18a4d7..5b9951f07b 100644
--- a/src/Common/src/System/IO/PathInternal.Unix.cs
+++ b/src/Common/src/System/IO/PathInternal.Unix.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System;
using System.Diagnostics;
using System.Text;
@@ -18,7 +19,7 @@ namespace System.IO
internal const string ParentDirectoryPrefix = @"../";
- internal static int GetRootLength(string path)
+ internal static int GetRootLength(ReadOnlySpan<char> path)
{
return path.Length > 0 && IsDirectorySeparator(path[0]) ? 1 : 0;
}
@@ -69,7 +70,7 @@ namespace System.IO
return builder.ToString();
}
-
+
/// <summary>
/// Returns true if the character is a directory or volume separator.
/// </summary>
@@ -82,5 +83,12 @@ namespace System.IO
Debug.Assert(Path.DirectorySeparatorChar == Path.VolumeSeparatorChar);
return ch == Path.DirectorySeparatorChar;
}
+
+ internal static bool IsPartiallyQualified(string path)
+ {
+ // This is much simpler than Windows where paths can be rooted, but not fully qualified (such as Drive Relative)
+ // As long as the path is rooted in Unix it doesn't use the current directory and therefore is fully qualified.
+ return string.IsNullOrEmpty(path) || path[0] != Path.DirectorySeparatorChar;
+ }
}
}
diff --git a/src/Common/src/System/Text/ValueStringBuilder.cs b/src/Common/src/System/Text/ValueStringBuilder.cs
index 0a91eb0f63..84e5fa8de9 100644
--- a/src/Common/src/System/Text/ValueStringBuilder.cs
+++ b/src/Common/src/System/Text/ValueStringBuilder.cs
@@ -21,7 +21,22 @@ namespace System.Text
_pos = 0;
}
- public int Length => _pos;
+ public int Length
+ {
+ get => _pos;
+ set
+ {
+ int delta = value - _pos;
+ if (delta > 0)
+ {
+ Append('\0', delta);
+ }
+ else
+ {
+ _pos = value;
+ }
+ }
+ }
public override string ToString()
{
diff --git a/src/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs b/src/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs
index 58435adece..d7878e8f55 100644
--- a/src/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs
+++ b/src/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs
@@ -212,5 +212,33 @@ namespace System.Text.Tests
Assert.Equal(Text2.Length, vsb.Length);
Assert.Equal(Text2, vsb.ToString());
}
+
+ [Fact]
+ public unsafe void Length_Growing_SetsNulls()
+ {
+ const string Text1 = "foobar";
+ var vsb = new ValueStringBuilder();
+
+ // Shrink then grow within capacity
+ vsb.Append(Text1);
+ Assert.Equal(Text1.Length, vsb.Length);
+ vsb.Length = 3;
+ Assert.Equal(3, vsb.Length);
+ vsb.Length = 6;
+ Assert.Equal(6, vsb.Length);
+ Assert.Equal("foo\0\0\0", vsb.ToString());
+
+ // Grow over capacity
+ const string Text2 = "bar";
+ Span<char> stackSpace = stackalloc char[Text2.Length];
+ var vsb2 = new ValueStringBuilder(stackSpace);
+ Assert.Equal(0, vsb2.Length);
+ vsb2.Append(Text2);
+ Assert.True(Text2.AsReadOnlySpan().SequenceEqual(stackSpace), "existing stack buffer should have been used");
+ Assert.Equal(Text2.Length, vsb2.Length);
+ vsb2.Length = 6;
+ Assert.Equal(6, vsb2.Length);
+ Assert.Equal("bar\0\0\0", vsb2.ToString());
+ }
}
}
diff --git a/src/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj b/src/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj
index 12357a11a2..734563e601 100644
--- a/src/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj
+++ b/src/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj
@@ -58,6 +58,9 @@
<Compile Include="System\IO\FileSystemWatcher.Win32.cs">
<SubType>Component</SubType>
</Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CloseHandle.cs">
+ <Link>Common\Interop\Windows\Interop.CloseHandle.cs</Link>
+ </Compile>
</ItemGroup>
<!-- Windows : Win32 only -->
<ItemGroup Condition="'$(TargetsWindows)' == 'true' and '$(TargetGroup)' != 'uap'">
@@ -162,4 +165,4 @@
<Reference Include="System.Collections" />
</ItemGroup>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
-</Project> \ No newline at end of file
+</Project>
diff --git a/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj b/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj
index 062639a22c..56b031de6a 100644
--- a/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj
+++ b/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj
@@ -18,6 +18,8 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'uap-Windows_NT-Debug|AnyCPU'" />
<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\Error.cs" />
<Compile Include="System\IO\Directory.cs" />
<Compile Include="System\IO\DirectoryInfo.cs" />
@@ -44,6 +46,9 @@
<Compile Include="$(CommonPath)\System\IO\StringBuilderCache.cs">
<Link>Common\System\IO\StringBuilderCache.cs</Link>
</Compile>
+ <Compile Include="$(CommonPath)\System\Text\ValueStringBuilder.cs">
+ <Link>Common\System\Text\ValueStringBuilder.cs</Link>
+ </Compile>
<Compile Include="$(CommonPath)\System\IO\PathInternal.cs">
<Link>Common\System\IO\PathInternal.cs</Link>
</Compile>
@@ -59,14 +64,21 @@
</ItemGroup>
<!-- Windows -->
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">
+ <Compile Include="System\IO\CharSpanExtensions.Windows.cs" />
<Compile Include="System\IO\DisableMediaInsertionPrompt.cs" />
- <Compile Include="Microsoft\Win32\SafeHandles\SafeFindHandle.Windows.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\Win32FileSystem.cs" />
- <Compile Include="System\IO\Win32FileSystemEnumerable.cs" />
+ <Compile Include="Microsoft\Win32\SafeHandles\SafeFindHandle.Windows.cs" />
<Compile Include="$(CommonPath)\Interop\Windows\Interop.Libraries.cs">
<Link>Common\Interop\Windows\Interop.Libraries.cs</Link>
</Compile>
@@ -195,6 +207,9 @@
<Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.DeleteFile.cs">
<Link>Common\Interop\Windows\Interop.DeleteFile.cs</Link>
</Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CompareStringOrdinal.cs">
+ <Link>Common\Interop\Windows\Interop.CompareStringOrdinal.cs</Link>
+ </Compile>
<Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CreateDirectory.cs">
<Link>Common\Interop\Windows\Interop.CreateDirectory.cs</Link>
</Compile>
@@ -222,15 +237,47 @@
<Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CopyFileEx.cs">
<Link>Common\Interop\Windows\Interop.CopyFileEx.cs</Link>
</Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\NtDll\Interop.FILE_FULL_DIR_INFORMATION.cs">
+ <Link>Common\Interop\Windows\Interop.FILE_FULL_DIR_INFORMATION.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\Interop.LongFileTime.cs">
+ <Link>Common\Interop\Windows\Interop.LongFileTime.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.FILE_INFO_BY_HANDLE_CLASS.cs">
+ <Link>Common\Interop\Windows\Interop.FILE_INFO_BY_HANDLE_CLASS.cs</Link>
+ </Compile>
</ItemGroup>
<!-- Windows : Win32 only -->
<ItemGroup Condition="'$(TargetsWindows)' == 'true' and '$(UWPCompatible)' != 'true'">
+ <Compile Include="System\IO\FindEnumerable.Win32.cs" />
<Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CreateFile.cs">
<Link>Common\Interop\Windows\Interop.CreateFile.cs</Link>
</Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\Interop.UNICODE_STRING.cs">
+ <Link>Common\Interop\Windows\Interop.UNICODE_STRING.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\Interop.BOOLEAN.cs">
+ <Link>Common\Interop\Windows\Interop.BOOLEAN.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\NtDll\Interop.IO_STATUS_BLOCK.cs">
+ <Link>Common\Interop\Windows\Interop.IO_STATUS_BLOCK.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\NtDll\Interop.NtQueryDirectoryFile.cs">
+ <Link>Common\Interop\Windows\Interop.NtQueryDirectoryFile.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\NtDll\Interop.FILE_INFORMATION_CLASS.cs">
+ <Link>Common\Interop\Windows\Interop.FILE_INFORMATION_CLASS.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\NtDll\Interop.NtStatus.cs">
+ <Link>Common\Interop\Windows\Interop.NtStatus.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\NtDll\Interop.RtlNtStatusToDosError.cs">
+ <Link>Common\Interop\Windows\Interop.RtlNtStatusToDosError.cs</Link>
+ </Compile>
</ItemGroup>
<!-- Windows : UAP - Win32 + WinRT -->
<ItemGroup Condition="'$(TargetsWindows)' == 'true' and '$(UWPCompatible)' == 'true'">
+ <Compile Include="System\IO\FindEnumerable.WinRT.cs" />
<Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CreateFile2.cs">
<Link>Common\Interop\Windows\Interop.CreateFile2.cs</Link>
</Compile>
@@ -240,9 +287,13 @@
<Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CREATEFILE2_EXTENDED_PARAMETERS.cs">
<Link>Common\Interop\Windows\Interop.CREATEFILE2_EXTENDED_PARAMETERS.cs</Link>
</Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.GetFileInformationByHandleEx.cs">
+ <Link>Common\Interop\Windows\Interop.GetFileInformationByHandleEx.cs</Link>
+ </Compile>
</ItemGroup>
<!-- Unix -->
<ItemGroup Condition="'$(TargetsUnix)' == 'true'">
+ <Compile Include="System\IO\CharSpanExtensions.Unix.cs" />
<Compile Include="System\IO\FileSystem.Current.Unix.cs" />
<Compile Include="System\IO\FileSystemInfo.Unix.cs" />
<Compile Include="System\IO\PathHelpers.Unix.cs" />
@@ -367,9 +418,10 @@
<Reference Include="System.Text.Encoding.Extensions" />
<Reference Include="System.Threading.Overlapped" />
<Reference Include="System.Threading.Tasks" />
+ <Reference Include="System.Threading" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsUnix)' == 'true'">
<Reference Include="System.Threading" />
</ItemGroup>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
-</Project> \ No newline at end of file
+</Project>
diff --git a/src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.Unix.cs b/src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.Unix.cs
new file mode 100644
index 0000000000..e985003d2e
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.Unix.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;
+
+namespace System.IO
+{
+ internal static partial class CharSpanExtensions
+ {
+ internal static unsafe bool EqualsOrdinal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, bool ignoreCase = false)
+ {
+ if (first.Length != second.Length)
+ return false;
+
+ if (!ignoreCase)
+ return first.SequenceEqual(second);
+
+ fixed (char* fp = &first.DangerousGetPinnableReference())
+ fixed (char* sp = &second.DangerousGetPinnableReference())
+ {
+ char* f = fp;
+ char* s = sp;
+
+ for (int i = 0; i < first.Length; i++)
+ {
+ if (*f != *s && char.ToUpperInvariant(*f) != char.ToUpperInvariant(*s))
+ return false;
+ f++;
+ s++;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.Windows.cs b/src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.Windows.cs
new file mode 100644
index 0000000000..a0c1481048
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.Windows.cs
@@ -0,0 +1,44 @@
+// 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.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+
+namespace System.IO
+{
+ internal static partial class CharSpanExtensions
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static unsafe int CompareOrdinal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, bool ignoreCase = false)
+ {
+ int result = Interop.Kernel32.CompareStringOrdinal(
+ ref first.DangerousGetPinnableReference(),
+ first.Length,
+ ref second.DangerousGetPinnableReference(),
+ second.Length,
+ ignoreCase);
+
+ if (result == 0)
+ throw Win32Marshal.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
+
+ // CSTR_LESS_THAN 1 // string 1 less than string 2
+ // CSTR_EQUAL 2 // string 1 equal to string 2
+ // CSTR_GREATER_THAN 3 // string 1 greater than string 2
+
+ return result - 2;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool EqualsOrdinal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, bool ignoreCase = false)
+ {
+ if (first.Length != second.Length)
+ return false;
+
+ if (!ignoreCase)
+ return first.SequenceEqual(second);
+
+ return CompareOrdinal(first, second, ignoreCase) == 0;
+ }
+ }
+}
diff --git a/src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.cs b/src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.cs
new file mode 100644
index 0000000000..80c6ba4160
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/CharSpanExtensions.cs
@@ -0,0 +1,24 @@
+// 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 CharSpanExtensions
+ {
+ public static bool EndsWithOrdinal(this ReadOnlySpan<char> span, ReadOnlySpan<char> value, bool ignoreCase = false)
+ {
+ if (value.Length == 0)
+ return true;
+ else if (value.Length > span.Length)
+ return false;
+
+ span = span.Slice(span.Length - value.Length);
+
+ if (ignoreCase == false)
+ return span.SequenceEqual(value);
+
+ return EqualsOrdinal(span, value, ignoreCase);
+ }
+ }
+}
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 b1d5f910fd..1c1ebd0197 100644
--- a/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.Windows.cs
+++ b/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.Windows.cs
@@ -3,17 +3,16 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
-using System.Security;
namespace System.IO
{
partial class DirectoryInfo
{
- internal DirectoryInfo(string fullPath, ref Interop.Kernel32.WIN32_FIND_DATA findData)
- : this(fullPath, findData.cFileName.GetStringFromFixedBuffer())
+ internal unsafe DirectoryInfo(string fullPath, string fileName, ref RawFindData findData)
+ : this(fullPath, fileName: fileName, isNormalized: true)
{
- Debug.Assert(findData.cFileName.FixedBufferEqualsString(Path.GetFileName(fullPath)));
- Init(ref findData);
+ Debug.Assert(fileName.Equals(Path.GetFileName(fullPath)));
+ Init(findData._info);
}
}
}
diff --git a/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs b/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs
index 9b6c948a25..d1630ccbf7 100644
--- a/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs
+++ b/src/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs
@@ -4,41 +4,43 @@
using System.Collections.Generic;
using System.Diagnostics;
-using System.Diagnostics.Contracts;
-using System.Security;
namespace System.IO
{
public sealed partial class DirectoryInfo : FileSystemInfo
{
+ private string _name;
+
public DirectoryInfo(string path)
{
- if (path == null)
- throw new ArgumentNullException(nameof(path));
- Contract.EndContractBlock();
+ Init(originalPath: PathHelpers.ShouldReviseDirectoryPathToCurrent(path) ? "." : path,
+ fullPath: Path.GetFullPath(path),
+ isNormalized: true);
+ }
- OriginalPath = PathHelpers.ShouldReviseDirectoryPathToCurrent(path) ? "." : path;
- FullPath = Path.GetFullPath(path);
- DisplayPath = GetDisplayName(OriginalPath);
+ internal DirectoryInfo(string originalPath, string fullPath = null, string fileName = null, bool isNormalized = false)
+ {
+ Init(originalPath, fullPath, fileName, isNormalized);
}
- internal DirectoryInfo(string fullPath, string originalPath)
+ private void Init(string originalPath, string fullPath = null, string fileName = null, bool isNormalized = false)
{
- Debug.Assert(Path.IsPathRooted(fullPath), "fullPath must be fully qualified!");
+ // Want to throw the original argument name
+ OriginalPath = originalPath ?? throw new ArgumentNullException("path");
+
+ fullPath = fullPath ?? originalPath;
+ Debug.Assert(!isNormalized || !PathInternal.IsPartiallyQualified(fullPath), "should be fully qualified if normalized");
+ fullPath = isNormalized ? fullPath : Path.GetFullPath(fullPath);
+
+ _name = fileName ?? (PathHelpers.IsRoot(fullPath) ?
+ fullPath :
+ Path.GetFileName(PathHelpers.TrimEndingDirectorySeparator(fullPath)));
- // Fast path when we know a DirectoryInfo exists.
- OriginalPath = originalPath ?? Path.GetFileName(fullPath);
FullPath = fullPath;
- DisplayPath = GetDisplayName(OriginalPath);
+ DisplayPath = PathHelpers.ShouldReviseDirectoryPathToCurrent(originalPath) ? "." : originalPath;
}
- public override string Name
- {
- get
- {
- return GetDirName(FullPath);
- }
- }
+ public override string Name => _name;
public DirectoryInfo Parent
{
@@ -66,7 +68,6 @@ namespace System.IO
{
if (path == null)
throw new ArgumentNullException(nameof(path));
- Contract.EndContractBlock();
return CreateSubdirectoryHelper(path);
}
@@ -122,7 +123,6 @@ namespace System.IO
{
if (searchPattern == null)
throw new ArgumentNullException(nameof(searchPattern));
- Contract.EndContractBlock();
return InternalGetFiles(searchPattern, SearchOption.TopDirectoryOnly);
}
@@ -135,7 +135,6 @@ namespace System.IO
throw new ArgumentNullException(nameof(searchPattern));
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories))
throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum);
- Contract.EndContractBlock();
return InternalGetFiles(searchPattern, searchOption);
}
@@ -169,7 +168,6 @@ namespace System.IO
{
if (searchPattern == null)
throw new ArgumentNullException(nameof(searchPattern));
- Contract.EndContractBlock();
return InternalGetFileSystemInfos(searchPattern, SearchOption.TopDirectoryOnly);
}
@@ -182,7 +180,6 @@ namespace System.IO
throw new ArgumentNullException(nameof(searchPattern));
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories))
throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum);
- Contract.EndContractBlock();
return InternalGetFileSystemInfos(searchPattern, searchOption);
}
@@ -212,7 +209,6 @@ namespace System.IO
{
if (searchPattern == null)
throw new ArgumentNullException(nameof(searchPattern));
- Contract.EndContractBlock();
return InternalGetDirectories(searchPattern, SearchOption.TopDirectoryOnly);
}
@@ -226,7 +222,6 @@ namespace System.IO
throw new ArgumentNullException(nameof(searchPattern));
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories))
throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum);
- Contract.EndContractBlock();
return InternalGetDirectories(searchPattern, searchOption);
}
@@ -252,7 +247,6 @@ namespace System.IO
{
if (searchPattern == null)
throw new ArgumentNullException(nameof(searchPattern));
- Contract.EndContractBlock();
return InternalEnumerateDirectories(searchPattern, SearchOption.TopDirectoryOnly);
}
@@ -263,7 +257,6 @@ namespace System.IO
throw new ArgumentNullException(nameof(searchPattern));
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories))
throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum);
- Contract.EndContractBlock();
return InternalEnumerateDirectories(searchPattern, searchOption);
}
@@ -285,7 +278,6 @@ namespace System.IO
{
if (searchPattern == null)
throw new ArgumentNullException(nameof(searchPattern));
- Contract.EndContractBlock();
return InternalEnumerateFiles(searchPattern, SearchOption.TopDirectoryOnly);
}
@@ -296,7 +288,6 @@ namespace System.IO
throw new ArgumentNullException(nameof(searchPattern));
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories))
throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum);
- Contract.EndContractBlock();
return InternalEnumerateFiles(searchPattern, searchOption);
}
@@ -318,7 +309,6 @@ namespace System.IO
{
if (searchPattern == null)
throw new ArgumentNullException(nameof(searchPattern));
- Contract.EndContractBlock();
return InternalEnumerateFileSystemInfos(searchPattern, SearchOption.TopDirectoryOnly);
}
@@ -329,7 +319,6 @@ namespace System.IO
throw new ArgumentNullException(nameof(searchPattern));
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories))
throw new ArgumentOutOfRangeException(nameof(searchOption), SR.ArgumentOutOfRange_Enum);
- Contract.EndContractBlock();
return InternalEnumerateFileSystemInfos(searchPattern, searchOption);
}
@@ -368,7 +357,6 @@ namespace System.IO
throw new ArgumentNullException(nameof(destDirName));
if (destDirName.Length == 0)
throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destDirName));
- Contract.EndContractBlock();
string destination = Path.GetFullPath(destDirName);
string destinationWithSeparator = destination;
@@ -401,9 +389,10 @@ namespace System.IO
FileSystem.Current.MoveDirectory(FullPath, destination);
- FullPath = destinationWithSeparator;
- OriginalPath = destDirName;
- DisplayPath = GetDisplayName(OriginalPath);
+ Init(originalPath: destDirName,
+ fullPath: destinationWithSeparator,
+ fileName: _name,
+ isNormalized: true);
// Flush any cached information about the directory.
Invalidate();
@@ -426,25 +415,5 @@ namespace System.IO
{
return DisplayPath;
}
-
- private static string GetDisplayName(string originalPath)
- {
- Debug.Assert(originalPath != null);
-
- // Desktop documents that the path returned by ToString() should be the original path.
- // For SL/Phone we only gave the directory name regardless of what was passed in.
- return PathHelpers.ShouldReviseDirectoryPathToCurrent(originalPath) ?
- "." :
- originalPath;
- }
-
- private static string GetDirName(string fullPath)
- {
- Debug.Assert(fullPath != null);
-
- return PathHelpers.IsRoot(fullPath) ?
- fullPath :
- Path.GetFileName(PathHelpers.TrimEndingDirectorySeparator(fullPath));
- }
}
}
diff --git a/src/System.IO.FileSystem/src/System/IO/DosMatcher.cs b/src/System.IO.FileSystem/src/System/IO/DosMatcher.cs
new file mode 100644
index 0000000000..b1f677efb6
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/DosMatcher.cs
@@ -0,0 +1,315 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Text;
+
+namespace System.IO
+{
+ internal static class DosMatcher
+ {
+ // [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
+ private static readonly char[] s_wildcardChars =
+ {
+ '\"', '<', '>', '*', '?'
+ };
+
+ /// <summary>
+ /// Change '*' and '?' to '&lt;', '&gt;' 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)
+ {
+ if (string.IsNullOrEmpty(expression) || expression == "*" || expression == "*.*")
+ return "*";
+
+ bool modified = false;
+ Span<char> stackSpace = stackalloc char[expression.Length];
+ ValueStringBuilder sb = new ValueStringBuilder(stackSpace);
+ int length = expression.Length;
+ for (int i = 0; i < length; i++)
+ {
+ char c = expression[i];
+ switch (c)
+ {
+ case '.':
+ modified = true;
+ if (i > 1 && i == length - 1 && expression[i - 1] == '*')
+ {
+ sb.Length--;
+ sb.Append('<'); // DOS_STAR (ends in *.)
+ }
+ else if (i < length - 1 && (expression[i + 1] == '?' || expression[i + 1] == '*'))
+ {
+ sb.Append('\"'); // DOS_DOT
+ }
+ else
+ {
+ sb.Append('.');
+ }
+ break;
+ case '?':
+ modified = true;
+ sb.Append('>'); // DOS_QM
+ break;
+ default:
+ sb.Append(c);
+ break;
+ }
+ }
+
+ return modified ? sb.ToString() : expression;
+ }
+
+ /// <summary>
+ /// Return true if the given expression matches the given name.
+ /// </summary>
+ /// <param name="expression">The expression to match with, such as "*.foo".</param>
+ /// <param name="name">The name to check against the expression.</param>
+ /// <param name="ignoreCase">True to ignore case (default).</param>
+ /// <remarks>
+ /// This is based off of System.IO.PatternMatcher used in FileSystemWatcher, which is based off
+ /// of RtlIsNameInExpression, which defines the rules for matching DOS wildcards ('*', '?', '&lt;', '&gt;', '"').
+ ///
+ /// Like PatternMatcher, matching will not line up with Win32 behavior unless you transform the expression
+ /// using <see cref="TranslateExpression(string)"/>
+ /// </remarks>
+ internal static bool MatchPattern(string expression, ReadOnlySpan<char> name, bool ignoreCase = true)
+ {
+ // 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)
+ return false;
+
+ if (expression[0] == '*')
+ {
+ // Just * matches everything
+ if (expression.Length == 1)
+ return true;
+
+ if (expression.IndexOfAny(s_wildcardChars, startIndex: 1) == -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)
+ return false;
+
+ // See if we end with the expression (minus the *, of course)
+ return name.EndsWithOrdinal(expression.AsReadOnlySpan().Slice(1), ignoreCase);
+ }
+ }
+
+ int nameOffset = 0;
+ int expressionOffset;
+
+ int priorMatch;
+ int currentMatch;
+ int priorMatchCount;
+ int matchCount = 1;
+
+ char nameChar = '\0';
+ char expressionChar;
+
+ Span<int> temp = stackalloc int[0];
+ Span<int> priorMatches = stackalloc int[16];
+ Span<int> currentMatches = stackalloc int[16];
+
+ int maxState = expression.Length * 2;
+ int currentState;
+ bool nameFinished = false;
+
+ while (!nameFinished)
+ {
+ if (nameOffset < name.Length)
+ {
+ // Not at the end of the name. Grab the current character and move the offset forward.
+ nameChar = name[nameOffset++];
+ }
+ else
+ {
+ // At the end of the name. If the expression is exhausted, exit.
+ if (priorMatches[matchCount - 1] == maxState)
+ break;
+
+ nameFinished = true;
+ }
+
+ // Now, for each of the previous stored expression matches, see what
+ // we can do with this name character.
+ priorMatch = 0;
+ currentMatch = 0;
+ priorMatchCount = 0;
+
+ while (priorMatch < matchCount)
+ {
+ // We have to carry on our expression analysis as far as possible for each
+ // character of name, so we loop here until the expression stops matching.
+
+ expressionOffset = (priorMatches[priorMatch++] + 1) / 2;
+
+ while (expressionOffset < expression.Length)
+ {
+ currentState = expressionOffset * 2;
+ expressionChar = expression[expressionOffset];
+
+ // We may be about to exhaust the local space for matches,
+ // so we have to reallocate if this is the case.
+ if (currentMatch >= currentMatches.Length - 2)
+ {
+ int newSize = currentMatches.Length * 2;
+ temp = new int[newSize];
+ currentMatches.CopyTo(temp);
+ currentMatches = temp;
+
+ temp = new int[newSize];
+ priorMatches.CopyTo(temp);
+ priorMatches = temp;
+ }
+
+ if (expressionChar == '*')
+ {
+ // '*' matches any character zero or more times.
+ goto MatchZeroOrMore;
+ }
+ else if (expressionChar == '<')
+ {
+ // '<' (DOS_STAR) matches any character except '.' zero or more times.
+
+ // If we are at a period, determine if we are allowed to
+ // consume it, i.e. make sure it is not the last one.
+
+ bool notLastPeriod = false;
+ if (!nameFinished && nameChar == '.')
+ {
+ for (int offset = nameOffset; offset < name.Length; offset++)
+ {
+ if (name[offset] == '.')
+ {
+ notLastPeriod = true;
+ break;
+ }
+ }
+ }
+
+ if (nameFinished || nameChar != '.' || notLastPeriod)
+ {
+ goto MatchZeroOrMore;
+ }
+ else
+ {
+ // We are at a period. We can only match zero
+ // characters (i.e. the epsilon transition).
+ goto MatchZero;
+ }
+ }
+ else
+ {
+ // The following expression characters all match by consuming
+ // a character, thus force the expression, and thus state forward.
+ currentState += 2;
+
+ if (expressionChar == '>')
+ {
+ // '>' (DOS_QM) is the most complicated. If the name is finished,
+ // we can match zero characters. If this name is a '.', we
+ // don't match, but look at the next expression. Otherwise
+ // we match a single character.
+ if (nameFinished || nameChar == '.')
+ goto NextExpressionCharacter;
+
+ currentMatches[currentMatch++] = currentState;
+ goto ExpressionFinished;
+ }
+ else if (expressionChar == '"')
+ {
+ // A '"' (DOS_DOT) can match either a period, or zero characters
+ // beyond the end of name.
+ if (nameFinished)
+ {
+ goto NextExpressionCharacter;
+ }
+ else if (nameChar == '.')
+ {
+ currentMatches[currentMatch++] = currentState;
+ }
+ goto ExpressionFinished;
+ }
+ else
+ {
+ // From this point on a name character is required to even
+ // continue, let alone make a match.
+ if (nameFinished) goto ExpressionFinished;
+
+ if (expressionChar == '?')
+ {
+ // If this expression was a '?' we can match it once.
+ currentMatches[currentMatch++] = currentState;
+ }
+ else if (ignoreCase
+ ? char.ToUpperInvariant(expressionChar) == char.ToUpperInvariant(nameChar)
+ : expressionChar == nameChar)
+ {
+ // Matched a non-wildcard character
+ currentMatches[currentMatch++] = currentState;
+ }
+
+ // The expression didn't match so move to the next prior match.
+ goto ExpressionFinished;
+ }
+ }
+
+ MatchZeroOrMore:
+ currentMatches[currentMatch++] = currentState;
+ MatchZero:
+ currentMatches[currentMatch++] = currentState + 1;
+ NextExpressionCharacter:
+ if (++expressionOffset == expression.Length)
+ currentMatches[currentMatch++] = maxState;
+ } // while (expressionOffset < expression.Length)
+
+ ExpressionFinished:
+
+ // Prevent duplication in the destination array.
+ //
+ // Each of the arrays is monotonically increasing and non-duplicating, thus we skip
+ // over any source element in the source array if we just added the same element to
+ // the destination array. This guarantees non-duplication in the destination array.
+
+ if ((priorMatch < matchCount) && (priorMatchCount < currentMatch))
+ {
+ while (priorMatchCount < currentMatch)
+ {
+ int previousLength = priorMatches.Length;
+ while ((priorMatch < previousLength) && (priorMatches[priorMatch] < currentMatches[priorMatchCount]))
+ {
+ priorMatch++;
+ }
+ priorMatchCount++;
+ }
+ }
+ } // while (sourceCount < matchesCount)
+
+ // If we found no matches in the just finished iteration it's time to bail.
+ if (currentMatch == 0)
+ return false;
+
+ // Swap the meaning the two arrays
+ temp = priorMatches;
+ priorMatches = currentMatches;
+ currentMatches = temp;
+
+ matchCount = currentMatch;
+ } // while (!nameFinished)
+
+ currentState = priorMatches[matchCount - 1];
+
+ return currentState == maxState;
+ }
+ }
+}
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 bb200232c2..cec962f3f4 100644
--- a/src/System.IO.FileSystem/src/System/IO/FileInfo.Windows.cs
+++ b/src/System.IO.FileSystem/src/System/IO/FileInfo.Windows.cs
@@ -3,17 +3,16 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
-using System.Security;
namespace System.IO
{
partial class FileInfo
{
- internal FileInfo(string fullPath, ref Interop.Kernel32.WIN32_FIND_DATA findData)
- : this(fullPath, findData.cFileName.GetStringFromFixedBuffer())
+ internal unsafe FileInfo(string fullPath, string fileName, ref RawFindData findData)
+ : this(fullPath, fileName: fileName, isNormalized: true)
{
- Debug.Assert(findData.cFileName.FixedBufferEqualsString(Path.GetFileName(fullPath)));
- Init(ref findData);
+ Debug.Assert(fileName.Equals(Path.GetFileName(fullPath)));
+ Init(findData._info);
}
}
}
diff --git a/src/System.IO.FileSystem/src/System/IO/FileInfo.cs b/src/System.IO.FileSystem/src/System/IO/FileInfo.cs
index 974ba57746..fa73e5204f 100644
--- a/src/System.IO.FileSystem/src/System/IO/FileInfo.cs
+++ b/src/System.IO.FileSystem/src/System/IO/FileInfo.cs
@@ -17,37 +17,21 @@ namespace System.IO
private FileInfo() { }
public FileInfo(string fileName)
+ : this(fileName, isNormalized: false)
{
- if (fileName == null)
- throw new ArgumentNullException(nameof(fileName));
- Contract.EndContractBlock();
-
- Init(fileName);
}
- private void Init(string fileName)
+ internal FileInfo(string originalPath, string fullPath = null, string fileName = null, bool isNormalized = false)
{
- OriginalPath = fileName;
- // Must fully qualify the path for the security check
- string fullPath = Path.GetFullPath(fileName);
+ // Want to throw the original argument name
+ OriginalPath = originalPath ?? throw new ArgumentNullException("fileName");
- _name = Path.GetFileName(fileName);
- FullPath = fullPath;
- DisplayPath = GetDisplayPath(fileName);
- }
+ fullPath = fullPath ?? originalPath;
+ Debug.Assert(!isNormalized || !PathInternal.IsPartiallyQualified(fullPath), "should be fully qualified if normalized");
- private string GetDisplayPath(string originalPath)
- {
- return originalPath;
- }
-
- internal FileInfo(string fullPath, string originalPath)
- {
- Debug.Assert(Path.IsPathRooted(fullPath), "fullPath must be fully qualified!");
- _name = originalPath ?? Path.GetFileName(fullPath);
- OriginalPath = _name;
- FullPath = fullPath;
- DisplayPath = _name;
+ FullPath = isNormalized ? fullPath ?? originalPath : Path.GetFullPath(fullPath);
+ _name = fileName ?? Path.GetFileName(originalPath);
+ DisplayPath = originalPath;
}
public override string Name
@@ -55,7 +39,6 @@ namespace System.IO
get { return _name; }
}
-
public long Length
{
get
@@ -135,10 +118,8 @@ namespace System.IO
throw new ArgumentNullException(nameof(destFileName), SR.ArgumentNull_FileName);
if (destFileName.Length == 0)
throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destFileName));
- Contract.EndContractBlock();
- destFileName = File.InternalCopy(FullPath, destFileName, false);
- return new FileInfo(destFileName, null);
+ return new FileInfo(File.InternalCopy(FullPath, destFileName, false), isNormalized: true);
}
@@ -159,8 +140,7 @@ namespace System.IO
throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destFileName));
Contract.EndContractBlock();
- destFileName = File.InternalCopy(FullPath, destFileName, overwrite);
- return new FileInfo(destFileName, null);
+ return new FileInfo(File.InternalCopy(FullPath, destFileName, overwrite), isNormalized: true);
}
public FileStream Create()
@@ -266,7 +246,7 @@ namespace System.IO
FullPath = fullDestFileName;
OriginalPath = destFileName;
_name = Path.GetFileName(fullDestFileName);
- DisplayPath = GetDisplayPath(destFileName);
+ DisplayPath = destFileName;
// Flush any cached information about the file.
Invalidate();
}
diff --git a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs
index 91b271bd4c..3510960ffd 100644
--- a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs
+++ b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs
@@ -17,10 +17,14 @@ namespace System.IO
// throw an appropriate error when attempting to access the cached info.
private int _dataInitialized = -1;
- internal void Init(ref Interop.Kernel32.WIN32_FIND_DATA findData)
+ internal unsafe void Init(Interop.NtDll.FILE_FULL_DIR_INFORMATION* info)
{
- // Copy the information to data
- _data.PopulateFrom(ref findData);
+ _data.dwFileAttributes = (int)info->FileAttributes;
+ _data.ftCreationTime = *((Interop.Kernel32.FILE_TIME*)&info->CreationTime);
+ _data.ftLastAccessTime = *((Interop.Kernel32.FILE_TIME*)&info->LastAccessTime);
+ _data.ftLastWriteTime = *((Interop.Kernel32.FILE_TIME*)&info->LastWriteTime);
+ _data.nFileSizeHigh = (uint)(info->EndOfFile >> 32);
+ _data.nFileSizeLow = (uint)info->EndOfFile;
_dataInitialized = 0;
}
diff --git a/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Win32.cs b/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Win32.cs
new file mode 100644
index 0000000000..a978d7cb85
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Win32.cs
@@ -0,0 +1,43 @@
+// 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.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace System.IO
+{
+ internal partial class FindEnumerable<TResult, TState>
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public unsafe bool GetData()
+ {
+ Debug.Assert(_directoryHandle != (IntPtr)(-1) && _directoryHandle != IntPtr.Zero && !_lastEntryFound);
+
+ int status = Interop.NtDll.NtQueryDirectoryFile(
+ FileHandle: _directoryHandle,
+ Event: IntPtr.Zero,
+ ApcRoutine: IntPtr.Zero,
+ ApcContext: IntPtr.Zero,
+ IoStatusBlock: out Interop.NtDll.IO_STATUS_BLOCK statusBlock,
+ FileInformation: _buffer,
+ Length: (uint)_buffer.Length,
+ FileInformationClass: Interop.NtDll.FILE_INFORMATION_CLASS.FileFullDirectoryInformation,
+ ReturnSingleEntry: Interop.BOOLEAN.FALSE,
+ FileName: null,
+ RestartScan: Interop.BOOLEAN.FALSE);
+
+ switch ((uint)status)
+ {
+ case Interop.StatusOptions.STATUS_NO_MORE_FILES:
+ DirectoryFinished();
+ return false;
+ case Interop.StatusOptions.STATUS_SUCCESS:
+ Debug.Assert(statusBlock.Information.ToInt64() != 0);
+ return true;
+ default:
+ throw Win32Marshal.GetExceptionForWin32Error((int)Interop.NtDll.RtlNtStatusToDosError(status), _currentPath);
+ }
+ }
+ }
+}
diff --git a/src/System.IO.FileSystem/src/System/IO/FindEnumerable.WinRT.cs b/src/System.IO.FileSystem/src/System/IO/FindEnumerable.WinRT.cs
new file mode 100644
index 0000000000..61e8b05d4d
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/FindEnumerable.WinRT.cs
@@ -0,0 +1,35 @@
+// 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.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace System.IO
+{
+ internal partial class FindEnumerable<TResult, TState>
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public unsafe bool GetData()
+ {
+ if (!Interop.Kernel32.GetFileInformationByHandleEx(
+ _directoryHandle,
+ Interop.Kernel32.FILE_INFO_BY_HANDLE_CLASS.FileFullDirectoryInfo,
+ _buffer,
+ (uint)_buffer.Length))
+ {
+ int error = Marshal.GetLastWin32Error();
+ switch (error)
+ {
+ case Interop.Errors.ERROR_NO_MORE_FILES:
+ DirectoryFinished();
+ return false;
+ default:
+ throw Win32Marshal.GetExceptionForWin32Error(error, _currentPath);
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Windows.cs b/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Windows.cs
new file mode 100644
index 0000000000..7e64e97373
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/FindEnumerable.Windows.cs
@@ -0,0 +1,251 @@
+// 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
new file mode 100644
index 0000000000..1202ac7372
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/FindEnumerableFactory.cs
@@ -0,0 +1,138 @@
+// 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
new file mode 100644
index 0000000000..2e52eda3db
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/FindPredicate.cs
@@ -0,0 +1,11 @@
+// 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
new file mode 100644
index 0000000000..9d90aed377
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/FindPredicates.cs
@@ -0,0 +1,18 @@
+// 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
new file mode 100644
index 0000000000..c1b6b71972
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/FindTransform.cs
@@ -0,0 +1,11 @@
+// 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
new file mode 100644
index 0000000000..db0590a35e
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/FindTransforms.cs
@@ -0,0 +1,40 @@
+// 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.Unix.cs b/src/System.IO.FileSystem/src/System/IO/PathHelpers.Unix.cs
index fb83930861..47dcd9b46d 100644
--- a/src/System.IO.FileSystem/src/System/IO/PathHelpers.Unix.cs
+++ b/src/System.IO.FileSystem/src/System/IO/PathHelpers.Unix.cs
@@ -13,21 +13,6 @@ namespace System.IO
return false;
}
- internal static void CheckSearchPattern(string searchPattern)
- {
- // ".." should not be used to move up directories. On Windows, this is more strict, and ".."
- // can only be used in particular places in a name, whereas on Unix it can be used anywhere.
- // So, throw if we find a ".." that's its own component in the path.
- for (int index = 0; (index = searchPattern.IndexOf("..", index, StringComparison.Ordinal)) >= 0; index += 2)
- {
- if ((index == 0 || PathInternal.IsDirectorySeparator(searchPattern[index - 1])) && // previous character is directory separator
- (index + 2 == searchPattern.Length || PathInternal.IsDirectorySeparator(searchPattern[index + 2]))) // next character is directory separator
- {
- throw new ArgumentException(SR.Arg_InvalidSearchPattern, nameof(searchPattern));
- }
- }
- }
-
internal static string TrimEndingDirectorySeparator(string path) =>
path.Length > 1 && PathInternal.IsDirectorySeparator(path[path.Length - 1]) ? // exclude root "/"
path.Substring(0, path.Length - 1) :
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 06d3f21765..8fa39312e5 100644
--- a/src/System.IO.FileSystem/src/System/IO/PathHelpers.Windows.cs
+++ b/src/System.IO.FileSystem/src/System/IO/PathHelpers.Windows.cs
@@ -2,80 +2,45 @@
// 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;
-
namespace System.IO
{
- internal static partial class PathHelpers
+ internal static partial class PathInternal
{
- // Trim trailing whitespace, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space.
- // string.WhitespaceChars will trim more aggressively than what the underlying FS does (for ex, NTFS, FAT).
- internal static readonly char[] TrimEndChars = { (char)0x9, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20, (char)0x85, (char)0xA0 };
- internal static readonly char[] TrimStartChars = { ' ' };
-
- internal static bool ShouldReviseDirectoryPathToCurrent(string path)
- {
- // In situations where this method is invoked, "<DriveLetter>:" should be special-cased
- // to instead go to the current directory.
- return path.Length == 2 && path[1] == ':';
- }
-
- // ".." can only be used if it is specified as a part of a valid File/Directory name. We disallow
- // the user being able to use it to move up directories. Here are some examples eg
- // Valid: a..b abc..d
- // Invalid: ..ab ab.. .. abc..d\abc..
- //
- internal static void CheckSearchPattern(string searchPattern)
+ internal static unsafe int GetRootLength(ReadOnlySpan<char> path)
{
- for (int index = 0; (index = searchPattern.IndexOf("..", index, StringComparison.Ordinal)) != -1; index += 2)
+ fixed (char* p = &path.DangerousGetPinnableReference())
{
- // Terminal ".." or "..\". File and directory names cannot end in "..".
- if (index + 2 == searchPattern.Length ||
- PathInternal.IsDirectorySeparator(searchPattern[index + 2]))
- {
- throw new ArgumentException(SR.Arg_InvalidSearchPattern, nameof(searchPattern));
- }
+ return (int)GetRootLength(p, (uint)path.Length);
}
}
+ }
- internal static string NormalizeSearchPattern(string searchPattern)
+ internal static partial class PathHelpers
+ {
+ internal static bool ShouldReviseDirectoryPathToCurrent(string path)
{
- Debug.Assert(searchPattern != null);
-
- // Win32 normalization trims only U+0020.
- string tempSearchPattern = searchPattern.TrimEnd(PathHelpers.TrimEndChars);
-
- // Make this corner case more useful, like dir
- if (tempSearchPattern.Equals("."))
- {
- tempSearchPattern = "*";
- }
-
- CheckSearchPattern(tempSearchPattern);
- return tempSearchPattern;
+ // In situations where this method is invoked, "<DriveLetter>:" should be special-cased
+ // to instead go to the current directory.
+ return path != null && path.Length == 2 && path[1] == ':';
}
- internal static string GetFullSearchString(string fullPath, string searchPattern)
- {
- Debug.Assert(fullPath != null);
- Debug.Assert(searchPattern != null);
+ internal static string TrimEndingDirectorySeparator(string path) =>
+ EndsInDirectorySeparator(path) ?
+ path.Substring(0, path.Length - 1) :
+ path;
- ThrowIfEmptyOrRootedPath(searchPattern);
- string tempStr = Path.Combine(fullPath, searchPattern);
+ public static bool IsPathRooted(string path)
+ {
+ // Want to avoid PathInternal.CheckInvalidPathChars on Path.IsPathRooted
- // If path ends in a trailing slash (\), append a * or we'll get a "Cannot find the file specified" exception
- char lastChar = tempStr[tempStr.Length - 1];
- if (PathInternal.IsDirectorySeparator(lastChar) || lastChar == Path.VolumeSeparatorChar)
+ if (path != null)
{
- tempStr = tempStr + "*";
+ int length = path.Length;
+ if ((length >= 1 && PathInternal.IsDirectorySeparator(path[0])) ||
+ (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == Path.VolumeSeparatorChar))
+ return true;
}
-
- return tempStr;
+ return false;
}
-
- internal static string TrimEndingDirectorySeparator(string path) =>
- EndsInDirectorySeparator(path) ?
- path.Substring(0, path.Length - 1) :
- path;
}
}
diff --git a/src/System.IO.FileSystem/src/System/IO/PathHelpers.cs b/src/System.IO.FileSystem/src/System/IO/PathHelpers.cs
index b6d966731e..58fb6e5ecd 100644
--- a/src/System.IO.FileSystem/src/System/IO/PathHelpers.cs
+++ b/src/System.IO.FileSystem/src/System/IO/PathHelpers.cs
@@ -2,6 +2,9 @@
// 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.Runtime.CompilerServices;
+
namespace System.IO
{
// Helper methods related to paths. Some of these are copies of
@@ -38,5 +41,151 @@ namespace System.IO
{
return path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1]);
}
+
+ /// <summary>
+ /// Combines two paths. Does no validation of paths, only concatenates the paths
+ /// and places a directory separator between them if needed.
+ /// </summary>
+ internal static string CombineNoChecks(string first, ReadOnlySpan<char> second)
+ {
+ if (string.IsNullOrEmpty(first))
+ return second.Length == 0
+ ? string.Empty
+ : new string(second);
+
+ if (second.Length == 0)
+ return first;
+
+ return CombineNoChecksInternal(first.AsReadOnlySpan(), second);
+ }
+
+ /// <summary>
+ /// Combines two paths. Does no validation of paths, only concatenates the paths
+ /// and places a directory separator between them if needed.
+ /// </summary>
+ internal static string CombineNoChecks(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
+ {
+ if (first.Length == 0)
+ return second.Length == 0
+ ? string.Empty
+ : new string(second);
+
+ if (second.Length == 0)
+ return new string(first);
+
+ return CombineNoChecksInternal(first, second);
+ }
+
+ /// <summary>
+ /// Combines three paths. Does no validation of paths, only concatenates the paths
+ /// and places a directory separator between them if needed.
+ /// </summary>
+ internal static string CombineNoChecks(string first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
+ {
+ if (string.IsNullOrEmpty(first))
+ return CombineNoChecks(second, third);
+
+ if (second.Length == 0)
+ return CombineNoChecks(first, third);
+
+ if (third.Length == 0)
+ return CombineNoChecks(first, second);
+
+ return CombineNoChecksInternal(first.AsReadOnlySpan(), second, third);
+ }
+
+ /// <summary>
+ /// Combines two paths. Does no validation of paths, only concatenates the paths
+ /// and places a directory separator between them if needed.
+ /// </summary>
+ internal unsafe static string CombineNoChecks(string first, string second)
+ {
+ if (string.IsNullOrEmpty(first))
+ return string.IsNullOrEmpty(second) ? string.Empty : second;
+
+ if (string.IsNullOrEmpty(second))
+ return first;
+
+ return CombineNoChecksInternal(first.AsReadOnlySpan(), second.AsReadOnlySpan());
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
+ {
+ Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");
+
+ bool hasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
+ || PathInternal.IsDirectorySeparator(second[0]);
+
+ fixed (char* f = &first.DangerousGetPinnableReference(), s = &second.DangerousGetPinnableReference())
+ {
+ return string.Create(
+ first.Length + second.Length + (hasSeparator ? 0 : 1),
+ (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length, HasSeparator: hasSeparator),
+ (destination, state) =>
+ {
+ new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
+ if (!state.HasSeparator)
+ destination[state.FirstLength] = Path.DirectorySeparatorChar;
+ new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.HasSeparator ? 0 : 1)));
+ });
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private unsafe static string CombineNoChecksInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
+ {
+ Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0, "should have dealt with empty paths");
+
+ bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
+ || PathInternal.IsDirectorySeparator(second[0]);
+ bool thirdHasSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
+ || PathInternal.IsDirectorySeparator(third[0]);
+
+ fixed (char* f = &first.DangerousGetPinnableReference(), s = &second.DangerousGetPinnableReference(), t = &third.DangerousGetPinnableReference())
+ {
+ return string.Create(
+ first.Length + second.Length + third.Length + (firstHasSeparator ? 0 : 1) + (thirdHasSeparator ? 0 : 1),
+ (First: (IntPtr)f, FirstLength: first.Length, Second: (IntPtr)s, SecondLength: second.Length,
+ Third: (IntPtr)t, ThirdLength: third.Length, FirstHasSeparator: firstHasSeparator, ThirdHasSeparator: thirdHasSeparator),
+ (destination, state) =>
+ {
+ new Span<char>((char*)state.First, state.FirstLength).CopyTo(destination);
+ if (!state.FirstHasSeparator)
+ destination[state.FirstLength] = Path.DirectorySeparatorChar;
+ new Span<char>((char*)state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.FirstHasSeparator ? 0 : 1)));
+ if (!state.ThirdHasSeparator)
+ destination[destination.Length - state.ThirdLength - 1] = Path.DirectorySeparatorChar;
+ new Span<char>((char*)state.Third, state.ThirdLength).CopyTo(destination.Slice(destination.Length - state.ThirdLength));
+ });
+ }
+ }
+
+ /// <summary>
+ /// Returns true if the file name is "." or ".."
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe bool IsDotOrDotDot(ReadOnlySpan<char> fileName)
+ {
+ return !(fileName.Length > 2
+ || fileName[0] != '.'
+ || (fileName.Length == 2 && fileName[1] != '.'));
+ }
+
+ public static ReadOnlySpan<char> GetDirectoryNameNoChecks(ReadOnlySpan<char> path)
+ {
+ if (path.Length == 0)
+ return ReadOnlySpan<char>.Empty;
+
+ int root = PathInternal.GetRootLength(path);
+ int i = path.Length;
+ if (i > root)
+ {
+ while (i > root && !PathInternal.IsDirectorySeparator(path[--i])) ;
+ return path.Slice(0, i);
+ }
+
+ return ReadOnlySpan<char>.Empty;
+ }
}
}
diff --git a/src/System.IO.FileSystem/src/System/IO/RawFindData.cs b/src/System.IO.FileSystem/src/System/IO/RawFindData.cs
new file mode 100644
index 0000000000..2f8ab43e42
--- /dev/null
+++ b/src/System.IO.FileSystem/src/System/IO/RawFindData.cs
@@ -0,0 +1,33 @@
+// 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/src/System/IO/UnixFileSystem.cs b/src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs
index 253a3ed2a8..cc3b72c69c 100644
--- a/src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs
+++ b/src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs
@@ -510,7 +510,6 @@ namespace System.IO
searchPattern = NormalizeSearchPattern(searchPattern);
if (searchPattern.Length > 0)
{
- PathHelpers.CheckSearchPattern(searchPattern);
PathHelpers.ThrowIfEmptyOrRootedPath(searchPattern);
// If the search pattern contains any paths, make sure we factor those into
diff --git a/src/System.IO.FileSystem/src/System/IO/Win32FileSystem.cs b/src/System.IO.FileSystem/src/System/IO/Win32FileSystem.cs
index 8e10fc11d6..3422c09149 100644
--- a/src/System.IO.FileSystem/src/System/IO/Win32FileSystem.cs
+++ b/src/System.IO.FileSystem/src/System/IO/Win32FileSystem.cs
@@ -186,22 +186,31 @@ namespace System.IO
public override IEnumerable<string> EnumeratePaths(string fullPath, string searchPattern, SearchOption searchOption, SearchTarget searchTarget)
{
- return Win32FileSystemEnumerableFactory.CreateFileNameIterator(fullPath, fullPath, searchPattern,
- (searchTarget & SearchTarget.Files) == SearchTarget.Files,
- (searchTarget & SearchTarget.Directories) == SearchTarget.Directories,
- searchOption);
+ 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 override IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string fullPath, string searchPattern, SearchOption searchOption, SearchTarget searchTarget)
{
+ FindEnumerableFactory.NormalizeInputs(ref fullPath, ref searchPattern);
switch (searchTarget)
{
case SearchTarget.Directories:
- return Win32FileSystemEnumerableFactory.CreateDirectoryInfoIterator(fullPath, fullPath, searchPattern, searchOption);
+ return FindEnumerableFactory.DirectoryInfos(fullPath, searchPattern, searchOption == SearchOption.AllDirectories);
case SearchTarget.Files:
- return Win32FileSystemEnumerableFactory.CreateFileInfoIterator(fullPath, fullPath, searchPattern, searchOption);
+ return FindEnumerableFactory.FileInfos(fullPath, searchPattern, searchOption == SearchOption.AllDirectories);
case SearchTarget.Both:
- return Win32FileSystemEnumerableFactory.CreateFileSystemInfoIterator(fullPath, fullPath, searchPattern, searchOption);
+ return FindEnumerableFactory.FileSystemInfos(fullPath, searchPattern, searchOption == SearchOption.AllDirectories);
default:
throw new ArgumentException(SR.ArgumentOutOfRange_Enum, nameof(searchTarget));
}
diff --git a/src/System.IO.FileSystem/src/System/IO/Win32FileSystemEnumerable.cs b/src/System.IO.FileSystem/src/System/IO/Win32FileSystemEnumerable.cs
deleted file mode 100644
index d75cfca5e6..0000000000
--- a/src/System.IO.FileSystem/src/System/IO/Win32FileSystemEnumerable.cs
+++ /dev/null
@@ -1,637 +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 Microsoft.Win32.SafeHandles;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Security;
-
-namespace System.IO
-{
- // Overview:
- // The key methods instantiate Win32FileSystemEnumerableIterators. These compose the iterator with search result
- // handlers that instantiate the FileInfo, DirectoryInfo, string, etc. The handlers then perform any
- // additional required permission demands.
- internal static class Win32FileSystemEnumerableFactory
- {
- internal static IEnumerable<string> CreateFileNameIterator(string path, string originalUserPath, string searchPattern,
- bool includeFiles, bool includeDirs, SearchOption searchOption)
- {
- Debug.Assert(path != null);
- Debug.Assert(originalUserPath != null);
- Debug.Assert(searchPattern != null);
-
- SearchResultHandler<string> handler;
-
- if (includeFiles && includeDirs)
- {
- handler = SearchResultHandler.FileSystemPath;
- }
- else if (includeFiles)
- {
- handler = SearchResultHandler.FilePath;
- }
- else
- {
- Debug.Assert(includeDirs, "Should never be excluding both files and directories.");
- handler = SearchResultHandler.DirectoryPath;
- }
-
- return new Win32FileSystemEnumerableIterator<string>(path, originalUserPath, searchPattern, searchOption, handler);
- }
-
- internal static IEnumerable<FileInfo> CreateFileInfoIterator(string path, string originalUserPath, string searchPattern, SearchOption searchOption)
- {
- Debug.Assert(path != null);
- Debug.Assert(originalUserPath != null);
- Debug.Assert(searchPattern != null);
-
- return new Win32FileSystemEnumerableIterator<FileInfo>(path, originalUserPath, searchPattern, searchOption, SearchResultHandler.FileInfo);
- }
-
- internal static IEnumerable<DirectoryInfo> CreateDirectoryInfoIterator(string path, string originalUserPath, string searchPattern, SearchOption searchOption)
- {
- Debug.Assert(path != null);
- Debug.Assert(originalUserPath != null);
- Debug.Assert(searchPattern != null);
-
- return new Win32FileSystemEnumerableIterator<DirectoryInfo>(path, originalUserPath, searchPattern, searchOption, SearchResultHandler.DirectoryInfo);
- }
-
- internal static IEnumerable<FileSystemInfo> CreateFileSystemInfoIterator(string path, string originalUserPath, string searchPattern, SearchOption searchOption)
- {
- Debug.Assert(path != null);
- Debug.Assert(originalUserPath != null);
- Debug.Assert(searchPattern != null);
-
- return new Win32FileSystemEnumerableIterator<FileSystemInfo>(path, originalUserPath, searchPattern, searchOption, SearchResultHandler.FileSystemInfo);
- }
- }
-
- // Overview:
- // Enumerates file system entries matching the search parameters. For recursive searches this
- // searches through all the sub dirs and executes the search criteria against every dir.
- //
- // Generic implementation:
- // Win32FileSystemEnumerableIterator is generic. When it gets a WIN32_FIND_DATA, it calls the
- // result handler to create an instance of the generic type.
- //
- // Usage:
- // Use Win32FileSystemEnumerableFactory to obtain FSEnumerables that can enumerate file system
- // entries as string path names, FileInfos, DirectoryInfos, or FileSystemInfos.
- //
- // Security:
- // For all the dirs/files returned, demands path discovery permission for their parent folders
- internal class Win32FileSystemEnumerableIterator<TSource> : Iterator<TSource>
- {
- private const int STATE_INIT = 1;
- private const int STATE_SEARCH_NEXT_DIR = 2;
- private const int STATE_FIND_NEXT_FILE = 3;
- private const int STATE_FINISH = 4;
-
- private readonly SearchResultHandler<TSource> _resultHandler;
- private List<PathPair> _searchList;
- private PathPair _searchData;
- private readonly string _searchCriteria;
- private SafeFindHandle _hnd = null;
-
- // empty means we know in advance that we won?t find any search results, which can happen if:
- // 1. we don't have a search pattern
- // 2. we're enumerating only the top directory and found no matches during the first call
- // This flag allows us to return early for these cases. We can?t know this in advance for
- // SearchOption.AllDirectories because we do a ?*? search for subdirs and then use the
- // searchPattern at each directory level.
- private bool _empty;
-
- private readonly string _userPath;
- private readonly SearchOption _searchOption;
- private readonly string _fullPath;
- private readonly string _normalizedSearchPath;
-
- internal Win32FileSystemEnumerableIterator(string path, string originalUserPath, string searchPattern, SearchOption searchOption, SearchResultHandler<TSource> resultHandler)
- {
- Debug.Assert(path != null);
- Debug.Assert(originalUserPath != null);
- Debug.Assert(searchPattern != null);
- Debug.Assert(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly);
- Debug.Assert(resultHandler != null);
-
- string normalizedSearchPattern = PathHelpers.NormalizeSearchPattern(searchPattern);
-
- if (normalizedSearchPattern.Length == 0)
- {
- _empty = true;
- }
- else
- {
- _resultHandler = resultHandler;
- _searchOption = searchOption;
-
- _fullPath = Path.GetFullPath(path);
- string fullSearchString = PathHelpers.GetFullSearchString(_fullPath, normalizedSearchPattern);
- _normalizedSearchPath = Path.GetDirectoryName(fullSearchString);
-
- // normalize search criteria
- _searchCriteria = GetNormalizedSearchCriteria(fullSearchString, _normalizedSearchPath);
-
- // fix up user path
- string searchPatternDirName = Path.GetDirectoryName(normalizedSearchPattern);
- _userPath = string.IsNullOrEmpty(searchPatternDirName) ?
- originalUserPath :
- Path.Combine(originalUserPath, searchPatternDirName);
-
- _searchData = new PathPair(_userPath, _normalizedSearchPath);
-
- CommonInit();
- }
- }
-
- private void CommonInit()
- {
- Debug.Assert(_searchCriteria != null, "searchCriteria should be initialized");
-
- // Execute searchCriteria against the current directory
- PathHelpers.ThrowIfEmptyOrRootedPath(_searchCriteria);
- string searchPath = Path.Combine(_searchData.FullPath, _searchCriteria);
-
- Interop.Kernel32.WIN32_FIND_DATA data = new Interop.Kernel32.WIN32_FIND_DATA();
-
- using (DisableMediaInsertionPrompt.Create())
- {
- // Open a Find handle
- _hnd = Interop.Kernel32.FindFirstFile(searchPath, ref data);
-
- if (_hnd.IsInvalid)
- {
- int errorCode = Marshal.GetLastWin32Error();
- if (errorCode != Interop.Errors.ERROR_FILE_NOT_FOUND && errorCode != Interop.Errors.ERROR_NO_MORE_FILES)
- {
- throw HandleError(errorCode, _searchData.FullPath);
- }
- else
- {
- // flag this as empty only if we're searching just top directory
- // Used in fast path for top directory only
- _empty = _searchOption == SearchOption.TopDirectoryOnly;
- }
- }
- }
-
- if (_searchOption == SearchOption.TopDirectoryOnly)
- {
- // fast path for TopDirectoryOnly. If we have a result, go ahead and set it to
- // current. If empty, dispose handle.
- if (_empty)
- {
- _hnd.Dispose();
- }
- else
- {
- TSource result;
- if (IsResultIncluded(ref data, out result))
- {
- current = result;
- }
- }
- }
- else
- {
- // for AllDirectories, we first recurse into dirs, so cleanup and add searchData
- // to the list
- _hnd.Dispose();
- _searchList = new List<PathPair>();
- _searchList.Add(_searchData);
- }
- }
-
- private Win32FileSystemEnumerableIterator(string fullPath, string normalizedSearchPath, string searchCriteria, string userPath, SearchOption searchOption, SearchResultHandler<TSource> resultHandler)
- {
- _fullPath = fullPath;
- _normalizedSearchPath = normalizedSearchPath;
- _searchCriteria = searchCriteria;
- _resultHandler = resultHandler;
- _userPath = userPath;
- _searchOption = searchOption;
-
- if (searchCriteria != null)
- {
- if (PathInternal.HasWildCardCharacters(fullPath))
- throw new ArgumentException(SR.Format(SR.Argument_InvalidPathChars, fullPath), nameof(fullPath));
-
- _searchData = new PathPair(userPath, normalizedSearchPath);
- CommonInit();
- }
- else
- {
- _empty = true;
- }
- }
-
- protected override Iterator<TSource> Clone()
- {
- return new Win32FileSystemEnumerableIterator<TSource>(_fullPath, _normalizedSearchPath, _searchCriteria, _userPath, _searchOption, _resultHandler);
- }
-
- protected override void Dispose(bool disposing)
- {
- try
- {
- _hnd?.Dispose();
- }
- finally
- {
- base.Dispose(disposing);
- }
- }
-
- public override bool MoveNext()
- {
- Interop.Kernel32.WIN32_FIND_DATA data = new Interop.Kernel32.WIN32_FIND_DATA();
- switch (state)
- {
- case STATE_INIT:
- {
- if (_empty)
- {
- state = STATE_FINISH;
- goto case STATE_FINISH;
- }
- if (_searchOption == SearchOption.TopDirectoryOnly)
- {
- state = STATE_FIND_NEXT_FILE;
- if (current != null)
- {
- return true;
- }
- else
- {
- goto case STATE_FIND_NEXT_FILE;
- }
- }
- else
- {
- state = STATE_SEARCH_NEXT_DIR;
- goto case STATE_SEARCH_NEXT_DIR;
- }
- }
- case STATE_SEARCH_NEXT_DIR:
- {
- Debug.Assert(_searchOption != SearchOption.TopDirectoryOnly, "should not reach this code path if searchOption == TopDirectoryOnly");
- Debug.Assert(_searchList != null, "_searchList should not be null");
-
- // Traverse directory structure. We need to get '*'
- while (_searchList.Count > 0)
- {
- int index = _searchList.Count - 1;
- _searchData = _searchList[index];
- Debug.Assert((_searchData.FullPath != null), "fullpath can't be null!");
- _searchList.RemoveAt(index);
-
- // Traverse the subdirs
- AddSearchableDirsToList(_searchData);
-
- // Execute searchCriteria against the current directory
- string searchPath = Path.Combine(_searchData.FullPath, _searchCriteria);
-
- using (DisableMediaInsertionPrompt.Create())
- {
- // Open a Find handle
- _hnd = Interop.Kernel32.FindFirstFile(searchPath, ref data);
-
- if (_hnd.IsInvalid)
- {
- int errorCode = Marshal.GetLastWin32Error();
- switch (errorCode)
- {
- case Interop.Errors.ERROR_FILE_NOT_FOUND:
- case Interop.Errors.ERROR_NO_MORE_FILES:
- case Interop.Errors.ERROR_PATH_NOT_FOUND:
- continue;
- }
-
- _hnd.Dispose();
- throw HandleError(errorCode, _searchData.FullPath);
- }
- }
-
- state = STATE_FIND_NEXT_FILE;
-
- TSource result;
- if (IsResultIncluded(ref data, out result))
- {
- current = result;
- return true;
- }
- else
- {
- goto case STATE_FIND_NEXT_FILE;
- }
- }
- state = STATE_FINISH;
- goto case STATE_FINISH;
- }
- case STATE_FIND_NEXT_FILE:
- {
- if (_hnd != null)
- {
- using (DisableMediaInsertionPrompt.Create())
- {
- // Keep asking for more matching files/dirs, add it to the list
- while (Interop.Kernel32.FindNextFile(_hnd, ref data))
- {
- TSource result;
- if (IsResultIncluded(ref data, out result))
- {
- current = result;
- return true;
- }
- }
- }
-
- // Make sure we quit with a sensible error.
- int errorCode = Marshal.GetLastWin32Error();
- _hnd?.Dispose();
-
- switch (errorCode)
- {
- case Interop.Errors.ERROR_SUCCESS:
- case Interop.Errors.ERROR_NO_MORE_FILES:
-
- // ERROR_FILE_NOT_FOUND is valid here because if the top level
- // dir doesn't contain any subdirs and matching files then
- // we will get here with this errorcode from the _searchList walk
- case Interop.Errors.ERROR_FILE_NOT_FOUND:
- break;
- default:
- throw HandleError(errorCode, _searchData.FullPath);
- }
- }
-
- if (_searchOption == SearchOption.TopDirectoryOnly)
- {
- state = STATE_FINISH;
- goto case STATE_FINISH;
- }
- else
- {
- state = STATE_SEARCH_NEXT_DIR;
- goto case STATE_SEARCH_NEXT_DIR;
- }
- }
- case STATE_FINISH:
- {
- Dispose();
- break;
- }
- }
- return false;
- }
-
- private bool IsResultIncluded(ref Interop.Kernel32.WIN32_FIND_DATA findData, out TSource result)
- {
- Debug.Assert(findData.cFileName.Length != 0 && !Path.IsPathRooted(findData.cFileName.GetStringFromFixedBuffer()),
- "Expected file system enumeration to not have empty file/directory name and not have rooted name");
-
- return _resultHandler.IsResultIncluded(_searchData.FullPath, _searchData.UserPath, ref findData, out result);
- }
-
- private Exception HandleError(int errorCode, string path)
- {
- Dispose();
- return Win32Marshal.GetExceptionForWin32Error(errorCode, path);
- }
-
- private void AddSearchableDirsToList(PathPair localSearchData)
- {
- string searchPath = Path.Combine(localSearchData.FullPath, "*");
- SafeFindHandle hnd = null;
- Interop.Kernel32.WIN32_FIND_DATA data = new Interop.Kernel32.WIN32_FIND_DATA();
- try
- {
- using (DisableMediaInsertionPrompt.Create())
- {
- // Get all files and dirs
- hnd = Interop.Kernel32.FindFirstFile(searchPath, ref data);
-
- if (hnd.IsInvalid)
- {
- int errorCode = Marshal.GetLastWin32Error();
-
- // This could happen if the dir doesn't contain any files.
- // Continue with the recursive search though, eventually
- // _searchList will become empty
- switch (errorCode)
- {
- case Interop.Errors.ERROR_FILE_NOT_FOUND:
- case Interop.Errors.ERROR_NO_MORE_FILES:
- case Interop.Errors.ERROR_PATH_NOT_FOUND:
- return;
- default:
- throw HandleError(errorCode, localSearchData.FullPath);
- }
- }
- }
-
- // Add subdirs to _searchList. Exempt ReparsePoints as appropriate
- Debug.Assert(_searchList != null, "_searchList should not be null");
- int initialCount = _searchList.Count;
- do
- {
- if (Win32FileSystemEnumerableHelpers.IsDir(ref data))
- {
- string fileName = data.cFileName.GetStringFromFixedBuffer();
-
- Debug.Assert(fileName.Length != 0 && !Path.IsPathRooted(fileName),
- "Expected file system enumeration to not have empty file/directory name and not have rooted name");
-
- string tempFullPath = Path.Combine(localSearchData.FullPath, fileName);
- string tempUserPath = Path.Combine(localSearchData.UserPath, fileName);
-
- // Setup search data for the sub directory and push it into the list
- PathPair searchDataSubDir = new PathPair(tempUserPath, tempFullPath);
-
- _searchList.Add(searchDataSubDir);
- }
- } while (Interop.Kernel32.FindNextFile(hnd, ref data));
-
- // Reverse the items just added to maintain FIFO order
- if (_searchList.Count > initialCount)
- {
- _searchList.Reverse(initialCount, _searchList.Count - initialCount);
- }
-
- // We don't care about errors here
- }
- finally
- {
- hnd?.Dispose();
- }
- }
-
- private static string GetNormalizedSearchCriteria(string fullSearchString, string fullPathMod)
- {
- Debug.Assert(fullSearchString != null);
- Debug.Assert(fullPathMod != null);
- Debug.Assert(fullSearchString.Length >= fullPathMod.Length);
-
- string searchCriteria = null;
- char lastChar = fullPathMod[fullPathMod.Length - 1];
- if (PathInternal.IsDirectorySeparator(lastChar))
- {
- // Can happen if the path is C:\temp, in which case GetDirectoryName would return C:\
- searchCriteria = fullSearchString.Substring(fullPathMod.Length);
- }
- else
- {
- Debug.Assert(fullSearchString.Length > fullPathMod.Length);
- searchCriteria = fullSearchString.Substring(fullPathMod.Length + 1);
- }
- return searchCriteria;
- }
- }
-
- internal abstract class SearchResultHandler<TSource>
- {
- /// <summary>
- /// Returns true if the result should be included. If true, the <paramref name="result"/> parameter
- /// is set to the created result object, otherwise it is set to null.
- /// </summary>
- internal abstract bool IsResultIncluded(string fullPath, string userPath, ref Interop.Kernel32.WIN32_FIND_DATA findData, out TSource result);
- }
-
- internal static class SearchResultHandler
- {
- private static SearchResultHandler<string> s_filePath;
- private static SearchResultHandler<string> s_directoryPath;
- private static SearchResultHandler<string> s_fileSystemPath;
- private static SearchResultHandler<FileInfo> s_fileInfo;
- private static SearchResultHandler<DirectoryInfo> s_directoryInfo;
- private static SearchResultHandler<FileSystemInfo> s_fileSystemInfo;
-
- internal static SearchResultHandler<string> FilePath
- {
- get { return s_filePath ?? (s_filePath = new StringResultHandler(includeFiles: true, includeDirs: false)); }
- }
-
- internal static SearchResultHandler<string> DirectoryPath
- {
- get { return s_directoryPath ?? (s_directoryPath = new StringResultHandler(includeFiles: false, includeDirs: true)); }
- }
-
- internal static SearchResultHandler<string> FileSystemPath
- {
- get { return s_fileSystemPath ?? (s_fileSystemPath = new StringResultHandler(includeFiles: true, includeDirs: true)); }
- }
-
- internal static SearchResultHandler<FileInfo> FileInfo
- {
- get { return s_fileInfo ?? (s_fileInfo = new FileInfoResultHandler()); }
- }
-
- internal static SearchResultHandler<DirectoryInfo> DirectoryInfo
- {
- get { return s_directoryInfo ?? (s_directoryInfo = new DirectoryInfoResultHandler()); }
- }
-
- internal static SearchResultHandler<FileSystemInfo> FileSystemInfo
- {
- get { return s_fileSystemInfo ?? (s_fileSystemInfo = new FileSystemInfoResultHandler()); }
- }
-
- private sealed class StringResultHandler : SearchResultHandler<string>
- {
- private readonly bool _includeFiles;
- private readonly bool _includeDirs;
-
- internal StringResultHandler(bool includeFiles, bool includeDirs)
- {
- _includeFiles = includeFiles;
- _includeDirs = includeDirs;
- }
-
- internal override bool IsResultIncluded(string fullPath, string userPath, ref Interop.Kernel32.WIN32_FIND_DATA findData, out string result)
- {
- if ((_includeFiles && Win32FileSystemEnumerableHelpers.IsFile(ref findData)) ||
- (_includeDirs && Win32FileSystemEnumerableHelpers.IsDir(ref findData)))
- {
- result = Path.Combine(userPath, findData.cFileName.GetStringFromFixedBuffer());
- return true;
- }
-
- result = null;
- return false;
- }
- }
-
- private sealed class FileInfoResultHandler : SearchResultHandler<FileInfo>
- {
- internal override bool IsResultIncluded(string fullPath, string userPath, ref Interop.Kernel32.WIN32_FIND_DATA findData, out FileInfo result)
- {
- if (Win32FileSystemEnumerableHelpers.IsFile(ref findData))
- {
- string fullPathFinal = Path.Combine(fullPath, findData.cFileName.GetStringFromFixedBuffer());
- result = new FileInfo(fullPathFinal, ref findData);
- return true;
- }
-
- result = null;
- return false;
- }
- }
-
- private sealed class DirectoryInfoResultHandler : SearchResultHandler<DirectoryInfo>
- {
- internal override bool IsResultIncluded(string fullPath, string userPath, ref Interop.Kernel32.WIN32_FIND_DATA findData, out DirectoryInfo result)
- {
- if (Win32FileSystemEnumerableHelpers.IsDir(ref findData))
- {
- string fullPathFinal = Path.Combine(fullPath, findData.cFileName.GetStringFromFixedBuffer());
- result = new DirectoryInfo(fullPathFinal, ref findData);
- return true;
- }
-
- result = null;
- return false;
- }
- }
-
- private sealed class FileSystemInfoResultHandler : SearchResultHandler<FileSystemInfo>
- {
- internal override bool IsResultIncluded(string fullPath, string userPath, ref Interop.Kernel32.WIN32_FIND_DATA findData, out FileSystemInfo result)
- {
- if (Win32FileSystemEnumerableHelpers.IsFile(ref findData))
- {
- string fullPathFinal = Path.Combine(fullPath, findData.cFileName.GetStringFromFixedBuffer());
- result = new FileInfo(fullPathFinal, ref findData);
- return true;
- }
- else if (Win32FileSystemEnumerableHelpers.IsDir(ref findData))
- {
- string fullPathFinal = Path.Combine(fullPath, findData.cFileName.GetStringFromFixedBuffer());
- result = new DirectoryInfo(fullPathFinal, ref findData);
- return true;
- }
-
- result = null;
- return false;
- }
- }
- }
-
- internal static class Win32FileSystemEnumerableHelpers
- {
- internal static bool IsDir(ref Interop.Kernel32.WIN32_FIND_DATA data)
- {
- // Don't add "." nor ".."
- return (0 != (data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY))
- && !data.cFileName.FixedBufferEqualsString(".") && !data.cFileName.FixedBufferEqualsString("..");
- }
-
- internal static bool IsFile(ref Interop.Kernel32.WIN32_FIND_DATA data)
- {
- return 0 == (data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY);
- }
- }
-}
diff --git a/src/System.IO.FileSystem/tests/Directory/EnumerableTests.cs b/src/System.IO.FileSystem/tests/Directory/EnumerableTests.cs
new file mode 100644
index 0000000000..e9c6028c56
--- /dev/null
+++ b/src/System.IO.FileSystem/tests/Directory/EnumerableTests.cs
@@ -0,0 +1,80 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ public class EnumerableTests : FileSystemTest
+ {
+ [Fact]
+ [ActiveIssue(25613, TestPlatforms.AnyUnix)]
+ public void FileEnumeratorIsThreadSafe()
+ {
+ string directory = Directory.CreateDirectory(GetTestFilePath()).FullName;
+ for (int i = 0; i < 100; i++)
+ File.Create(Path.Combine(directory, GetTestFileName())).Dispose();
+
+ // We are really only trying to make sure we don't terminate the process.
+ // Throwing IOException at this point to try and flush out other problems
+ // like bad handles. Can narrow the throw if this isn't reliable.
+
+ try
+ {
+ new ThreadSafeRepro().Execute(directory);
+ }
+ catch (Exception e) when (!(e is IOException))
+ {
+ }
+ }
+
+ class ThreadSafeRepro
+ {
+ volatile IEnumerator<string> _enumerator;
+
+ void Enumerate(IEnumerator<string> s)
+ {
+ while (s.MoveNext())
+ { }
+ s.Dispose();
+ }
+
+ public void Execute(string directory)
+ {
+ CancellationTokenSource source = new CancellationTokenSource();
+ CancellationToken token = source.Token;
+
+ void Work()
+ {
+ do
+ {
+ IEnumerator<string> x = _enumerator;
+ if (x != null)
+ Enumerate(x);
+ } while (!token.IsCancellationRequested);
+ }
+
+ Task taskOne = Task.Run(action: Work);
+ Task taskTwo = Task.Run(action: Work);
+
+ try
+ {
+ for (int i = 0; i < 1000; i++)
+ {
+ _enumerator = Directory.EnumerateFiles(directory).GetEnumerator();
+ Enumerate(_enumerator);
+ }
+ }
+ finally
+ {
+ source.Cancel();
+ Task.WaitAll(taskOne, taskTwo);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.IO.FileSystem/tests/Directory/GetFileSystemEntries_str_str.cs b/src/System.IO.FileSystem/tests/Directory/GetFileSystemEntries_str_str.cs
index 408cff3424..45eb340755 100644
--- a/src/System.IO.FileSystem/tests/Directory/GetFileSystemEntries_str_str.cs
+++ b/src/System.IO.FileSystem/tests/Directory/GetFileSystemEntries_str_str.cs
@@ -191,7 +191,7 @@ namespace System.IO.Tests
[Fact]
public void SearchPatternIgnoreSubDirectories()
{
- //Shouldn't get files on full path by default
+ // Shouldn't get files on full path by default
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
Directory.CreateDirectory(Path.Combine(testDir.FullName, GetTestFileName()));
using (File.Create(Path.Combine(testDir.FullName, GetTestFileName())))
@@ -409,8 +409,8 @@ namespace System.IO.Tests
ValidatePatternMatch(expected, GetEntries(testDir, pattern));
}
- [ActiveIssue(20781, TestPlatforms.AnyUnix)]
[OuterLoop("These are pretty corner, don't need to run all the time.")]
+ [SkipOnTargetFramework(~TargetFrameworkMonikers.NetFramework)]
// Can't do these without extended path support on Windows, UsingNewNormalization filters appropriately
[ConditionalTheory(nameof(UsingNewNormalization)),
// "foo*." actually becomes "foo<" when passed to NT. It matches all characters up to, and including, the final period.
@@ -561,7 +561,142 @@ namespace System.IO.Tests
// Really should be: new string[] { @"foo.." }), but is
new string[] { @"foo. ", @"foo. ", @"foo. ", @"foo.." }),
]
- public void PatternTests_DosStarOddSpace(string pattern, string[] sourceFiles, string[] expected)
+ public void PatternTests_DosStarOddSpace_Desktop(string pattern, string[] sourceFiles, string[] expected)
+ {
+ // Tests for DOS_STAR, which only occurs when the source pattern ends in *.
+ // These cases don't match documented behavior on Windows- matching *should* end at the final period.
+
+ // We don't want to eat trailing space/periods in this test
+ string testDir = PrepareDirectory(sourceFiles, useExtendedPaths: true);
+ ValidatePatternMatch(expected, GetEntries(testDir, pattern));
+ }
+
+ [ActiveIssue(20781, TestPlatforms.AnyUnix)]
+ [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+ [OuterLoop("These are pretty corner, don't need to run all the time.")]
+ [Theory,
+ // "foo*." actually becomes "foo<" when passed to NT. It matches all characters up to, and including, the final period.
+ //
+ // There is a "bug" somewhere in the Windows stack where *some* files with trailing spaces after the final period will be returned when
+ // using "*." at the end of a string (which becomes "<"). According to the rules (and the actual pattern matcher used FsRtlIsNameInExpression)
+ // *nothing* should match after the final period.
+ //
+ // We've made Core effectively call RtlIsNameInExpression directly, so this test validates the normally buggy cases. See the test above
+ // for what Windows really does. These are super obscure and the bug pattern isn't obvious so we're just going with "correct".
+ InlineData(
+ "foo*.",
+ new string[] { @"foo", @"foo.", @"foo.t", @"foo.tx", @"foo.txt", @"bar.txt", @"foo..", @"foo...", @"foo. ", @"foo. ", @"foo .", @"foo. . .", @"foo. t" },
+ new string[] { @"foo", @"foo.", @"foo..", @"foo...", @"foo .", @"foo. . ." }),
+ InlineData(
+ "*.",
+ new string[] { @"foo. ", @"foo. ", @"foo..", @"foo. t" },
+ new string[] { @"foo.." }),
+ InlineData(
+ "f*.",
+ new string[] { @"foo. ", @"foo. ", @"foo..", @"foo. t" },
+ new string[] { @"foo.." }),
+ InlineData(
+ "fo*.",
+ new string[] { @"foo. ", @"foo. ", @"foo..", @"foo. t" },
+ new string[] { @"foo.." }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo. ", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo." },
+ new string[] { @"foo." }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo.", @"foo. ", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { @"foo." }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo.", @"foo", @"foo. ", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { @"foo.", @"foo" }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo.", @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo" },
+ new string[] { @"foo.", @"foo" }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo. ", @"foo", @"foo.", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { @"foo.", @"foo" }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo. ", @"foo", @"food", @"foo.", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { @"foo.", @"foo", @"food" }),
+ InlineData(
+ "fo*.",
+ new string[] { @"foo.", @"foo. ", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { @"foo." }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo. ", @"foo. .", @"foo. . ", @"foo. . .", @"foo. . . " },
+ new string[] { @"foo. .", @"foo. . ." }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo. ", @"foo. .", @"foo.. .", @"foo.... .", @"foo..... ." },
+ new string[] { @"foo. .", @"foo.. .", @"foo.... .", @"foo..... ." }),
+ InlineData(
+ "fo*.",
+ new string[] { @"foo. ", @"foo. .", @"foo. . ", @"foo. . .", @"foo. . . " },
+ new string[] { @"foo. .", @"foo. . ."}),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo.", @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { @"foo." }),
+ InlineData(
+ "food*.",
+ new string[] { @"food.", @"food. ", @"food. ", @"food. ", @"food. ", @"food. " },
+ new string[] { @"food." }),
+ InlineData(
+ "food*.",
+ new string[] { @"food.", @"food. ", @"food. ", @"food. ", @"food. ", @"food. ", @"foodi." },
+ new string[] { @"food.", @"foodi." }),
+ InlineData(
+ "foodi*.",
+ new string[] { @"foodi.", @"foodi. ", @"foodi. ", @"foodi. ", @"foodi. ", @"foodi. " },
+ new string[] { @"foodi." }),
+ InlineData(
+ "foodie*.",
+ new string[] { @"foodie.", @"foodie. ", @"foodie. ", @"foodie. ", @"foodie. ", @"foodie. " },
+ new string[] { @"foodie." }),
+ InlineData(
+ "fooooo*.",
+ new string[] { @"foooooo.", @"foooooo. ", @"foooooo. " },
+ new string[] { @"foooooo." }),
+ InlineData(
+ "fooooo*.",
+ new string[] { @"foooooo. ", @"foooooo. ", @"foooooo. " },
+ new string[] { }),
+ InlineData(
+ "fo*.",
+ new string[] { @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { }),
+ InlineData(
+ "fo*.",
+ new string[] { @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { }),
+ InlineData(
+ "fo*.",
+ new string[] { @"fo. ", @"fo. ", @"fo. ", @"fo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { }),
+ InlineData(
+ "fo*.",
+ new string[] { @"fo. ", @"fo. ", @"fo. ", @"fo. ", @"fo. ", @"fo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. ", @"foo. " },
+ new string[] { }),
+ InlineData(
+ "foo*.",
+ new string[] { @"foo. ", @"foo. ", @"foo..", @"foo. t", @"foo. ", @"foo. " },
+ new string[] { @"foo.." }),
+ ]
+ public void PatternTests_DosStarOddSpace_Core(string pattern, string[] sourceFiles, string[] expected)
{
// Tests for DOS_STAR, which only occurs when the source pattern ends in *.
// These cases don't match documented behavior on Windows- matching *should* end at the final period.
@@ -607,24 +742,21 @@ namespace System.IO.Tests
}
[Fact]
- [PlatformSpecific(TestPlatforms.Windows)]
+ [PlatformSpecific(TestPlatforms.Windows)]
public void WindowsSearchPatternLongSegment()
{
// Create a path segment longer than the normal max of 255
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
string longName = new string('k', 257);
-
- // Long path segment in search pattern throws PathTooLongException on Desktop,
- // otherwise it is an IOException.
+
+ // Long path segment in search pattern throws PathTooLongException on Desktop
if (PlatformDetection.IsFullFramework)
{
Assert.Throws<PathTooLongException>(() => GetEntries(testDir.FullName, longName));
}
else
{
- var exception = Assert.Throws<IOException>(() => GetEntries(testDir.FullName, longName));
- // Should be Interop.Errors.ERROR_INVALID_PARAMETER converted to a HResult
- Assert.Equal(unchecked((int)0x80070057), exception.HResult);
+ GetEntries(testDir.FullName, longName);
}
}
@@ -651,20 +783,38 @@ namespace System.IO.Tests
}
[Fact]
- [PlatformSpecific(TestPlatforms.Windows)] // Search pattern with double dots throws ArgumentException
- public void WindowsSearchPatternWithDoubleDots()
+ [SkipOnTargetFramework(~TargetFrameworkMonikers.NetFramework)]
+ public void WindowsSearchPatternWithDoubleDots_Desktop()
{
+ // Search pattern with double dots throws ArgumentException
Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, Path.Combine("..ab ab.. .. abc..d", "abc..")));
Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, ".."));
Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, @".." + Path.DirectorySeparatorChar));
}
+ [Fact]
+ [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+ public void SearchPatternWithDoubleDots_Core()
+ {
+ // Search pattern with double dots no longer throws ArgumentException
+ string directory = Directory.CreateDirectory(GetTestFilePath()).FullName;
+ Assert.Throws<DirectoryNotFoundException>(() => GetEntries(directory, Path.Combine("..ab ab.. .. abc..d", "abc..")));
+ GetEntries(directory, "..");
+ GetEntries(directory, @".." + Path.DirectorySeparatorChar);
+
+ Assert.Throws<DirectoryNotFoundException>(() => GetEntries(directory, Path.Combine("..ab ab.. .. abc..d", "abc", "..")));
+ GetEntries(directory, Path.Combine("..ab ab.. .. abc..d", "..", "abc"));
+ Assert.Throws<DirectoryNotFoundException>(() => GetEntries(directory, Path.Combine("..", "..ab ab.. .. abc..d", "abc")));
+ Assert.Throws<DirectoryNotFoundException>(() => GetEntries(directory, Path.Combine("..", "..ab ab.. .. abc..d", "abc") + Path.DirectorySeparatorChar));
+ }
+
private static char[] OldWildcards = new char[] { '*', '?' };
private static char[] NewWildcards = new char[] { '<', '>', '\"' };
[Fact]
[PlatformSpecific(TestPlatforms.Windows)] // Windows-invalid search patterns throw
- public void WindowsSearchPatternInvalid()
+ [SkipOnTargetFramework(~TargetFrameworkMonikers.NetFramework)]
+ public void WindowsSearchPatternInvalid_Desktop()
{
Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, "\0"));
Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, "|"));
@@ -701,6 +851,29 @@ namespace System.IO.Tests
}
[Fact]
+ [PlatformSpecific(TestPlatforms.Windows)]
+ [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+ public void WindowsSearchPatternInvalid_Core()
+ {
+ GetEntries(TestDirectory, "\0");
+ GetEntries(TestDirectory, "|");
+
+ Assert.All(Path.GetInvalidFileNameChars().Except(OldWildcards).Except(NewWildcards), invalidChar =>
+ {
+ switch (invalidChar)
+ {
+ case '\\':
+ case '/':
+ Assert.Throws<DirectoryNotFoundException>(() => GetEntries(Directory.GetCurrentDirectory(), string.Format("te{0}st", invalidChar.ToString())));
+ break;
+ default:
+ GetEntries(Directory.GetCurrentDirectory(), string.Format("te{0}st", invalidChar.ToString()));
+ break;
+ }
+ });
+ }
+
+ [Fact]
[PlatformSpecific(TestPlatforms.Windows)] // Windows-invalid search patterns throw
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "In netcoreapp we made three new characters be treated as valid wildcards instead of invalid characters. NetFX still treats them as InvalidChars.")]
public void WindowsSearchPatternInvalid_Wildcards_netcoreapp()
@@ -840,22 +1013,6 @@ namespace System.IO.Tests
}
}
- [Fact]
- [PlatformSpecific(TestPlatforms.AnyUnix)] // Search pattern with DoubleDots on Unix
- public void UnixSearchPatternWithDoubleDots()
- {
- // search pattern is valid but directory doesn't exist
- Assert.Throws<DirectoryNotFoundException>(() => GetEntries(TestDirectory, Path.Combine("..ab ab.. .. abc..d", "abc..")));
-
- // invalid search pattern trying to go up a directory with ..
- Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, ".."));
- Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, @".." + Path.DirectorySeparatorChar));
- Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, Path.Combine("..ab ab.. .. abc..d", "abc", "..")));
- Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, Path.Combine("..ab ab.. .. abc..d", "..", "abc")));
- Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, Path.Combine("..", "..ab ab.. .. abc..d", "abc")));
- Assert.Throws<ArgumentException>(() => GetEntries(TestDirectory, Path.Combine("..", "..ab ab.. .. abc..d", "abc") + Path.DirectorySeparatorChar));
- }
-
#endregion
}
}
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 fce0d4d915..b6637446b0 100644
--- a/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj
+++ b/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj
@@ -24,6 +24,7 @@
<Compile Include="Base\InfoGetSetTimes.cs" />
<Compile Include="Base\AllGetSetAttributes.cs" />
<Compile Include="Base\StaticGetSetTimes.cs" />
+ <Compile Include="Directory\EnumerableTests.cs" />
<Compile Include="FileInfo\GetSetAttributesCommon.cs" />
<Compile Include="FileInfo\IsReadOnly.cs" />
<Compile Include="FileInfo\Replace.cs" />
diff --git a/src/System.IO.IsolatedStorage/tests/System/IO/IsolatedStorage/GetFileNamesTests.cs b/src/System.IO.IsolatedStorage/tests/System/IO/IsolatedStorage/GetFileNamesTests.cs
index 16d84a11d0..a7485584a4 100644
--- a/src/System.IO.IsolatedStorage/tests/System/IO/IsolatedStorage/GetFileNamesTests.cs
+++ b/src/System.IO.IsolatedStorage/tests/System/IO/IsolatedStorage/GetFileNamesTests.cs
@@ -53,7 +53,20 @@ namespace System.IO.IsolatedStorage
}
[Fact]
- public void GetFileNames_RaisesInvalidPath()
+ [ActiveIssue(25428, TestPlatforms.AnyUnix)]
+ [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+ public void GetFileNames_RaisesInvalidPath_Core()
+ {
+ // We are no longer as agressive with filters for enumerating files
+ using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForAssembly())
+ {
+ isf.GetFileNames("\0bad");
+ }
+ }
+
+ [Fact]
+ [SkipOnTargetFramework(~TargetFrameworkMonikers.NetFramework)]
+ public void GetFileNames_RaisesInvalidPath_Desktop()
{
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForAssembly())
{
diff --git a/src/System.IO.Ports/src/System.IO.Ports.csproj b/src/System.IO.Ports/src/System.IO.Ports.csproj
index f0722438e6..c5f4f027aa 100644
--- a/src/System.IO.Ports/src/System.IO.Ports.csproj
+++ b/src/System.IO.Ports/src/System.IO.Ports.csproj
@@ -127,6 +127,9 @@
<Compile Include="$(CommonPath)\Interop\Windows\Interop.BOOL.cs">
<Link>Common\Interop\Windows\Interop.BOOL.cs</Link>
</Compile>
+ <Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CloseHandle.cs">
+ <Link>Common\Interop\Windows\kernel32\Interop.CloseHandle.cs</Link>
+ </Compile>
</ItemGroup>
<ItemGroup Condition="'$(TargetGroup)' != 'uap' and '$(TargetsWindows)' == 'true'">
<Compile Include="$(CommonPath)\Interop\Windows\kernel32\Interop.CreateFile.cs">
@@ -167,4 +170,4 @@
<Reference Include="System.Threading.ThreadPool" />
</ItemGroup>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
-</Project> \ No newline at end of file
+</Project>