Add tracking a recursion depth in System.IO.FileSystem (#48148)
authorIlya <darpa@yandex.ru>
Wed, 31 Mar 2021 08:02:48 +0000 (13:02 +0500)
committerGitHub <noreply@github.com>
Wed, 31 Mar 2021 08:02:48 +0000 (01:02 -0700)
* Add tracking a depth recursion in System.IO.FileSystem

* Update src/libraries/System.IO.FileSystem/src/System/IO/EnumerationOptions.cs

Co-authored-by: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com>
* Fix docs

* Address feedbacks

* Fix typo

* Add MaxRecursionDepth value check

* Fix net48 build: Add string to MS.IO.Redist resx file

* Fix test and initialization.

* Address feedback

Co-authored-by: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com>
Co-authored-by: David Cantu <dacantu@microsoft.com>
src/libraries/Microsoft.IO.Redist/src/Resources/Strings.resx
src/libraries/System.IO.FileSystem/ref/System.IO.FileSystem.cs
src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Unix.cs
src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Windows.cs
src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.cs
src/libraries/System.IO.FileSystem/src/System/IO/EnumerationOptions.cs
src/libraries/System.IO.FileSystem/tests/Enumeration/RecursionDepthTests.cs [new file with mode: 0644]
src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj

index d5c13ce..f84023f 100644 (file)
     <value>The path is empty.</value>
   </data>
   <data name="Arg_MustBeDriveLetterOrRootDir" xml:space="preserve">
-    <value>Drive name must be a root directory (i.e. 'C:\') or a drive letter ('C').</value> 
+    <value>Drive name must be a root directory (i.e. 'C:\') or a drive letter ('C').</value>
+  </data>
+  <data name="ArgumentOutOfRange_NeedNonNegNum" xml:space="preserve">
+    <value>Non-negative number required.</value>
   </data>
 </root>
index 64c2bbc..08121dc 100644 (file)
@@ -101,6 +101,7 @@ namespace System.IO
         public bool IgnoreInaccessible { get { throw null; } set { } }
         public System.IO.MatchCasing MatchCasing { get { throw null; } set { } }
         public System.IO.MatchType MatchType { get { throw null; } set { } }
+        public int MaxRecursionDepth { get { throw null; } set { } }
         public bool RecurseSubdirectories { get { throw null; } set { } }
         public bool ReturnSpecialDirectories { get { throw null; } set { } }
     }
index 95bb53f..ae179b0 100644 (file)
@@ -22,7 +22,7 @@ namespace System.IO.Enumeration
         private string? _currentPath;
         private IntPtr _directoryHandle;
         private bool _lastEntryFound;
-        private Queue<string>? _pending;
+        private Queue<(string Path, int RemainingDepth)>? _pending;
 
         private Interop.Sys.DirectoryEntry _entry;
         private TResult? _current;
@@ -144,12 +144,12 @@ namespace System.IO.Enumeration
 
                         if (isDirectory && !isSpecialDirectory)
                         {
-                            if (_options.RecurseSubdirectories && ShouldRecurseIntoEntry(ref entry))
+                            if (_options.RecurseSubdirectories && _remainingRecursionDepth > 0 && ShouldRecurseIntoEntry(ref entry))
                             {
                                 // Recursion is on and the directory was accepted, Queue it
                                 if (_pending == null)
-                                    _pending = new Queue<string>();
-                                _pending.Enqueue(Path.Join(_currentPath, entry.FileName));
+                                    _pending = new Queue<(string Path, int RemainingDepth)>();
+                                _pending.Enqueue((Path.Join(_currentPath, entry.FileName), _remainingRecursionDepth - 1));
                             }
                         }
 
@@ -215,7 +215,7 @@ namespace System.IO.Enumeration
                 if (_pending == null || _pending.Count == 0)
                     return false;
 
-                _currentPath = _pending.Dequeue();
+                (_currentPath, _remainingRecursionDepth) = _pending.Dequeue();
                 _directoryHandle = CreateDirectoryHandle(_currentPath, ignoreNotFound: true);
             }
 
index 32fd206..a6ac202 100644 (file)
@@ -38,7 +38,7 @@ namespace System.IO.Enumeration
         private IntPtr _directoryHandle;
         private string? _currentPath;
         private bool _lastEntryFound;
-        private Queue<(IntPtr Handle, string Path)>? _pending;
+        private Queue<(IntPtr Handle, string Path, int RemainingDepth)>? _pending;
 
         private void Init()
         {
@@ -161,7 +161,7 @@ namespace System.IO.Enumeration
                             if (!_options.ReturnSpecialDirectories)
                                 continue;
                         }
-                        else if (_options.RecurseSubdirectories && ShouldRecurseIntoEntry(ref entry))
+                        else if (_options.RecurseSubdirectories && _remainingRecursionDepth > 0 && ShouldRecurseIntoEntry(ref entry))
                         {
                             // Recursion is on and the directory was accepted, Queue it
                             string subDirectory = Path.Join(_currentPath.AsSpan(), _entry->FileName);
@@ -171,8 +171,8 @@ namespace System.IO.Enumeration
                                 try
                                 {
                                     if (_pending == null)
-                                        _pending = new Queue<(IntPtr, string)>();
-                                    _pending.Enqueue((subDirectoryHandle, subDirectory));
+                                        _pending = new Queue<(IntPtr, string, int)>();
+                                    _pending.Enqueue((subDirectoryHandle, subDirectory, _remainingRecursionDepth - 1));
                                 }
                                 catch
                                 {
@@ -209,7 +209,7 @@ namespace System.IO.Enumeration
             if (_pending == null || _pending.Count == 0)
                 return false;
 
-            (_directoryHandle, _currentPath) = _pending.Dequeue();
+            (_directoryHandle, _currentPath, _remainingRecursionDepth) = _pending.Dequeue();
             return true;
         }
 
index b5168eb..5a626a8 100644 (file)
@@ -14,6 +14,8 @@ namespace System.IO.Enumeration
 {
     public unsafe abstract partial class FileSystemEnumerator<TResult> : CriticalFinalizerObject, IEnumerator<TResult>
     {
+        private int _remainingRecursionDepth;
+
         /// <summary>
         /// Encapsulates a find operation.
         /// </summary>
@@ -37,6 +39,7 @@ namespace System.IO.Enumeration
             string path = isNormalized ? directory : Path.GetFullPath(directory);
             _rootDirectory = Path.TrimEndingDirectorySeparator(path);
             _options = options ?? EnumerationOptions.Default;
+            _remainingRecursionDepth = _options.MaxRecursionDepth;
 
             Init();
         }
index ca6b035..e3d6ad1 100644 (file)
@@ -12,6 +12,10 @@ namespace System.IO
 {
     public class EnumerationOptions
     {
+        private int _maxRecursionDepth;
+
+        internal const int DefaultMaxRecursionDepth = int.MaxValue;
+
         /// <summary>
         /// For internal use. These are the options we want to use if calling the existing Directory/File APIs where you don't
         /// explicitly specify EnumerationOptions.
@@ -34,6 +38,7 @@ namespace System.IO
         {
             IgnoreInaccessible = true;
             AttributesToSkip = FileAttributes.Hidden | FileAttributes.System;
+            MaxRecursionDepth = DefaultMaxRecursionDepth;
         }
 
         /// <summary>
@@ -96,6 +101,24 @@ namespace System.IO
         /// </remarks>
         public MatchCasing MatchCasing { get; set; }
 
+        /// <summary>Gets or sets a value that indicates the maximum directory depth to recurse while enumerating, when <see cref="RecurseSubdirectories" /> is set to <see langword="true" />.</summary>
+        /// <value>A number that represents the maximum directory depth to recurse while enumerating. The default value is <see cref="int.MaxValue" />.</value>
+        /// <remarks>If <see cref="MaxRecursionDepth" /> is set to a negative number, the default value <see cref="int.MaxValue" /> is used.
+        /// If <see cref="MaxRecursionDepth" /> is set to zero, enumeration returns the contents of the initial directory.</remarks>
+        public int MaxRecursionDepth
+        {
+            get => _maxRecursionDepth;
+            set
+            {
+                if (value < 0)
+                {
+                    throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum);
+                }
+
+                _maxRecursionDepth = value;
+            }
+        }
+
         /// <summary>
         /// Set to true to return "." and ".." directory entries.
         /// </summary>
diff --git a/src/libraries/System.IO.FileSystem/tests/Enumeration/RecursionDepthTests.cs b/src/libraries/System.IO.FileSystem/tests/Enumeration/RecursionDepthTests.cs
new file mode 100644 (file)
index 0000000..5ac0e61
--- /dev/null
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO.Enumeration;
+using System.Linq;
+using Xunit;
+
+namespace System.IO.Tests.Enumeration
+{
+    public class RecursionDepthTests : FileSystemTest
+    {
+        public static IEnumerable<string> GetEntryNames(string directory, int depth)
+        {
+            return new FileSystemEnumerable<string>(
+                directory,
+                (ref FileSystemEntry entry) => entry.FileName.ToString(),
+                new EnumerationOptions() { RecurseSubdirectories = true, MaxRecursionDepth = depth });
+        }
+
+        [Theory,
+            InlineData(0, 2),
+            InlineData(1, 4),
+            InlineData(2, 5),
+            InlineData(3, 5),
+            InlineData(int.MaxValue, 5)
+        ]
+        public void EnumerateDirectory_WithSpecifedRecursionDepth(int depth, int expectedCount)
+        {
+            DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath());
+            DirectoryInfo testSubdirectory1 = Directory.CreateDirectory(Path.Combine(testDirectory.FullName, "Subdirectory1"));
+            DirectoryInfo testSubdirectory2 = Directory.CreateDirectory(Path.Combine(testSubdirectory1.FullName, "Subdirectory2"));
+            FileInfo fileOne = new FileInfo(Path.Combine(testDirectory.FullName, "fileone.htm"));
+            FileInfo fileTwo = new FileInfo(Path.Combine(testSubdirectory1.FullName, "filetwo.html"));
+            FileInfo fileThree = new FileInfo(Path.Combine(testSubdirectory2.FullName, "filethree.doc"));
+
+            fileOne.Create().Dispose();
+            fileTwo.Create().Dispose();
+            fileThree.Create().Dispose();
+
+            string[] results = GetEntryNames(testDirectory.FullName, depth).ToArray();
+            Assert.Equal(expectedCount, results.Length);
+        }
+
+        [Fact]
+        public void NegativeRecursionDepth_ThrowsArgumentOutOfRangeException()
+        {
+            Assert.Throws<ArgumentOutOfRangeException>(() => new EnumerationOptions() { MaxRecursionDepth = -1 });
+        }
+    }
+}
index 7a81b2e..97b5036 100644 (file)
@@ -32,6 +32,7 @@
     <Compile Include="FileStream\DisposeAsync.cs" />
     <Compile Include="FileStream\ReadWriteSpan.cs" />
     <Compile Include="Enumeration\ConstructionTests.cs" />
+    <Compile Include="Enumeration\RecursionDepthTests.cs" />
     <Compile Include="Enumeration\SpecialDirectoryTests.cs" />
     <Compile Include="Enumeration\SkipAttributeTests.cs" />
     <Compile Include="Enumeration\FileSystemNameTests.cs" />