Files
SabreTools.IO/SabreTools.IO.Extensions/IOExtensions.cs
gmipf 6dddf7db37 Do not follow linked directories in SafeGetFiles outside of Windows (#8)
* Do not follow linked directories in SafeGetFiles outside of Windows

SafeGetFiles with SearchOption.AllDirectories follows directory symlinks.
On Linux, the UDF view of some PC/Mac hybrid discs has symlinks whose
target reads as empty, and each one resolves to its own folder, so the
recursion never ends (fixes #2, SabreTools/BinaryObjectScanner#276).

Outside of Windows, walk the tree one directory at a time and do not enter
directories that carry FileAttributes.ReparsePoint. Files are matched with
Directory.GetFiles per directory, so the pattern behaves as before, and the
order is the same breadth-first order. Windows keeps Directory.GetFiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Do not follow linked directories in the other AllDirectories overloads

SafeGetDirectories and the SafeEnumerateDirectories, SafeEnumerateFiles
and SafeEnumerateFileSystemEntries wrappers follow directory symlinks in
the same way. On Linux, with one directory linked to itself,
SafeEnumerateFiles returns 43 files instead of 2, and with three such
links it is still running after 10 seconds.

The walk from SafeGetFiles is now a lazy iterator that all of them share,
so the Enumerate wrappers stay lazy. Each directory is listed with the
matching overload without SearchOption (Directory.GetFiles,
SafeEnumerateFiles and so on), so patterns and inaccessible directories
behave as in the recursive search. If the attributes of a subdirectory
cannot be read, such as below a directory without execute permission, it
is entered anyway, so it fails in the same way as in the recursive search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Check for linked directories without catching exceptions

DirectoryInfo.Exists returns false where the attributes cannot be read, and the criteria for the walk are checked one at a time with their reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 13:52:11 +01:00

1065 lines
42 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using SabreTools.Collections.Extensions;
using SabreTools.Text.Compare;
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Methods around path operations
/// </summary>
public static class IOExtensions
{
#region Directory
/// <summary>
/// Ensure the output directory is a proper format and can be created
/// </summary>
/// <param name="dir">Directory to check</param>
/// <param name="create">True if the directory should be created, false otherwise (default)</param>
/// <returns>Full path to the directory</returns>
public static string EnsureDirectory(this string? dir, bool create = false)
{
// If the output directory is invalid
if (string.IsNullOrEmpty(dir))
dir = PathTool.GetRuntimeDirectory();
// Get the full path for the output directory
dir = Path.GetFullPath(dir!.Trim('"'));
// If we're creating the output folder, do so
if (create && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
return dir;
}
/// <summary>
/// Retrieve a list of just directories from inputs
/// </summary>
/// <param name="inputs">List of strings representing directories and files</param>
/// <param name="appendParent">True if the parent name should be included in the ParentablePath, false otherwise (default)</param>
/// <returns>List of strings representing just directories from the inputs</returns>
public static List<ParentablePath> GetDirectoriesOnly(List<string> inputs, bool appendParent = false)
{
var outputs = new List<ParentablePath>();
for (int i = 0; i < inputs.Count; i++)
{
string input = inputs[i];
// If we have a null or empty path
if (string.IsNullOrEmpty(input))
continue;
// If we have a wildcard
string pattern = "*";
if (input.Contains("*") || input.Contains("?"))
{
pattern = Path.GetFileName(input);
input = input.Substring(0, input.Length - pattern.Length);
}
// Get the parent path in case of appending
string parentPath = Path.GetFullPath(input);
if (Directory.Exists(input))
{
List<string> directories = input.GetDirectoriesOrdered(pattern);
foreach (string dir in directories)
{
outputs.Add(new ParentablePath(Path.GetFullPath(dir), appendParent ? parentPath : string.Empty));
}
}
}
return outputs;
}
/// <summary>
/// Retrieve a list of directories from a directory recursively in proper order
/// </summary>
/// <param name="dir">Directory to parse</param>
/// <param name="pattern">Optional pattern to search for directory names</param>
/// <returns>List with all new files</returns>
public static List<string> GetDirectoriesOrdered(this string dir, string pattern = "*")
=> GetDirectoriesOrderedHelper(dir, [], pattern);
/// <summary>
/// Retrieve a list of directories from a directory recursively in proper order
/// </summary>
/// <param name="dir">Directory to parse</param>
/// <param name="infiles">List representing existing files</param>
/// <param name="pattern">Optional pattern to search for directory names</param>
/// <returns>List with all new files</returns>
private static List<string> GetDirectoriesOrderedHelper(string dir, List<string> infiles, string pattern)
{
// Take care of the files in the top directory
List<string> toadd = [.. dir.SafeEnumerateDirectories(pattern, SearchOption.TopDirectoryOnly)];
toadd.Sort(new NaturalComparer());
infiles.AddRange(toadd);
// Then recurse through and add from the directories
foreach (string subDir in toadd)
{
infiles = GetDirectoriesOrderedHelper(subDir, infiles, pattern);
}
// Return the new list
return infiles;
}
/// <summary>
/// Get all empty folders within a root folder
/// </summary>
/// <param name="root">Root directory to parse</param>
/// <returns>IEumerable containing all directories that are empty, an empty enumerable if the root is empty, null otherwise</returns>
public static List<string>? ListEmpty(this string? root)
{
// Check null or empty first
if (root is null)
return null;
// Then, check if the root exists
if (!Directory.Exists(root))
return null;
// Otherwise, get the complete list
var empty = new List<string>();
foreach (var dir in SafeGetDirectories(root, "*", SearchOption.AllDirectories))
{
if (SafeGetFiles(dir).Length == 0)
empty.Add(dir);
}
return empty;
}
#endregion
#region File
/// <summary>
/// Concatenate all files in the order provided, if possible
/// </summary>
/// <param name="paths">List of paths to combine</param>
/// <param name="output">Path to the output file</param>
/// <returns>True if the files were concatenated successfully, false otherwise</returns>
public static bool Concatenate(List<string> paths, string output)
{
// If the path list is empty
if (paths.Count == 0)
return false;
// If the output filename is invalid
if (string.IsNullOrEmpty(output))
return false;
try
{
// Try to build the new output file
using var ofs = File.Open(output, FileMode.Create, FileAccess.Write, FileShare.None);
for (int i = 0; i < paths.Count; i++)
{
// Get the next file
string next = paths[i];
if (!File.Exists(next))
break;
// Copy the next input to the output
using var ifs = File.Open(next, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
ifs.BlockCopy(ofs, blockSize: 3 * 1024 * 1024);
}
return true;
}
catch
{
// Absorb the exception right now
return false;
}
}
/// <summary>
/// Determines a text file's encoding by analyzing its byte order mark (BOM).
/// Defaults to ASCII when detection of the text file's endianness fails.
/// </summary>
/// <param name="filename">The text file to analyze.</param>
/// <returns>The detected encoding.</returns>
/// <link>http://stackoverflow.com/questions/3825390/effective-way-to-find-any-files-encoding</link>
public static Encoding GetEncoding(this string filename)
{
if (string.IsNullOrEmpty(filename))
return Encoding.Default;
if (!File.Exists(filename))
return Encoding.Default;
// Try to open the file
try
{
var file = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
if (file is null)
return Encoding.Default;
// Read the BOM
var bom = new byte[4];
int read = file.Read(bom, 0, 4);
file.Dispose();
// Disable warning about UTF7 usage
#if NET5_0_OR_GREATER
#pragma warning disable SYSLIB0001
#endif
// Analyze the BOM
if (bom[0] == 0x2b && bom[1] == 0x2f && bom[2] == 0x76) return Encoding.UTF7;
if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf) return Encoding.UTF8;
if (bom[0] == 0xff && bom[1] == 0xfe) return Encoding.Unicode; //UTF-16LE
if (bom[0] == 0xfe && bom[1] == 0xff) return Encoding.BigEndianUnicode; //UTF-16BE
if (bom[0] == 0 && bom[1] == 0 && bom[2] == 0xfe && bom[3] == 0xff) return Encoding.UTF32;
return Encoding.Default;
// Restore warning about UTF7 usage
#if NET5_0_OR_GREATER
#pragma warning restore SYSLIB0001
#endif
}
catch
{
return Encoding.Default;
}
}
/// <summary>
/// Retrieve a list of just files from inputs
/// </summary>
/// <param name="inputs">List of strings representing directories and files</param>
/// <param name="appendParent">True if the parent name should be be included in the ParentablePath, false otherwise (default)</param>
/// <returns>List of strings representing just files from the inputs</returns>
public static List<ParentablePath> GetFilesOnly(List<string> inputs, bool appendParent = false)
{
var outputs = new List<ParentablePath>();
for (int i = 0; i < inputs.Count; i++)
{
string input = inputs[i].Trim('"');
// If we have a null or empty path
if (string.IsNullOrEmpty(input))
continue;
// If we have a wildcard
string pattern = "*";
if (input.Contains("*") || input.Contains("?"))
{
pattern = Path.GetFileName(input);
input = input.Substring(0, input.Length - pattern.Length);
}
// Get the parent path in case of appending
string parentPath = Path.GetFullPath(input);
if (Directory.Exists(input))
{
List<string> files = input.GetFilesOrdered(pattern);
foreach (string file in files)
{
outputs.Add(new ParentablePath(Path.GetFullPath(file), appendParent ? parentPath : string.Empty));
}
}
else if (File.Exists(input))
{
outputs.Add(new ParentablePath(Path.GetFullPath(input), appendParent ? parentPath : string.Empty));
}
}
return outputs;
}
/// <summary>
/// Retrieve a list of files from a directory recursively in proper order
/// </summary>
/// <param name="dir">Directory to parse</param>
/// <param name="pattern">Optional pattern to search for directory names</param>
/// <returns>List with all new files</returns>
public static List<string> GetFilesOrdered(this string dir, string pattern = "*")
=> GetFilesOrderedHelper(dir, [], pattern);
/// <summary>
/// Retrieve a list of files from a directory recursively in proper order
/// </summary>
/// <param name="dir">Directory to parse</param>
/// <param name="infiles">List representing existing files</param>
/// <param name="pattern">Optional pattern to search for directory names</param>
/// <returns>List with all new files</returns>
private static List<string> GetFilesOrderedHelper(string dir, List<string> infiles, string pattern)
{
// Take care of the files in the top directory
List<string> toadd = [.. dir.SafeEnumerateFiles(pattern, SearchOption.TopDirectoryOnly)];
toadd.Sort(new NaturalComparer());
infiles.AddRange(toadd);
// Then recurse through and add from the directories
List<string> subDirs = [.. dir.SafeEnumerateDirectories(pattern, SearchOption.TopDirectoryOnly)];
subDirs.Sort(new NaturalComparer());
foreach (string subdir in subDirs)
{
infiles = GetFilesOrderedHelper(subdir, infiles, pattern);
}
// Return the new list
return infiles;
}
/// <summary>
/// Helper to get the filesize from a path
/// </summary>
/// <returns>Size of the file path, -1 on error</returns>
public static long GetFileSize(this string? filename)
{
// Invalid filenames are ignored
if (string.IsNullOrEmpty(filename))
return -1;
// Non-file paths are ignored
if (!File.Exists(filename))
return -1;
try
{
return new FileInfo(filename).Length;
}
catch
{
// Ignore errors
return -1;
}
}
/// <summary>
/// Get the extension from the path, if possible
/// </summary>
/// <param name="path">Path to get extension from</param>
/// <returns>Extension, if possible</returns>
public static string? GetNormalizedExtension(this string? path)
{
// Check null or empty first
if (string.IsNullOrEmpty(path))
return null;
// Get the extension from the path, if possible
string? ext = Path.GetExtension(path)?.ToLowerInvariant();
// Check if the extension is null or empty
if (string.IsNullOrEmpty(ext))
return null;
// Make sure that extensions are valid
ext = ext!.TrimStart('.');
return ext;
}
/// <inheritdoc cref="NormalizeFilePath(string?, bool)"/>
public static string NormalizeFilePath(this string? path)
=> path.NormalizeFilePath(fullPath: true);
/// <summary>
/// Normalize a file path
/// </summary>
/// <param name="path">Path value to normalize</param>
/// <param name="fullPath">Indicates if the full path is used</param>
/// <returns>The normalized path on success, an empty path if null, or the original path on error</returns>
/// <remarks>
/// This method performs the following steps:
/// - Remove quotes and angle brackets from the start and end
/// - Replaces invalid path characters with '_'
/// - Remove spaces before and after directory separators
/// </remarks>
public static string NormalizeFilePath(this string? path, bool fullPath)
{
try
{
// If we have an invalid path
if (string.IsNullOrEmpty(path))
return string.Empty;
// Remove quotes and angle brackets from path
path = path!.Trim('\"');
path = path!.Trim('<');
path = path!.Trim('>');
// Remove invalid path characters
foreach (char c in Path.GetInvalidPathChars())
{
path = path.Replace(c, '_');
}
// Try getting the combined path and returning that directly
string usablePath = fullPath ? Path.GetFullPath(path) : path;
var fullDirectory = Path.GetDirectoryName(usablePath)?.Trim();
string fullFile = Path.GetFileName(usablePath).Trim();
// Remove invalid filename characters
foreach (char c in Path.GetInvalidFileNameChars())
{
fullFile = fullFile.Replace(c, '_');
}
// Rebuild the path, if necessary
if (!string.IsNullOrEmpty(fullDirectory))
fullFile = Path.Combine(fullDirectory, fullFile);
// Remove spaces before and after separators
return Regex.Replace(fullFile, @"\s*([\\|/])\s*", @"$1");
}
catch { }
return path ?? string.Empty;
}
/// <summary>
/// Resolve a file path that may be absolute, relative,
/// within the runtime directory, or contained within a PATH directory.
/// </summary>
/// <param name="path">Raw value from the user's options</param>
/// <returns>The absolute path of the located files, or null on failure</returns>
public static string? ResolvePath(this string? path)
{
// Invalid paths always return null
if (string.IsNullOrEmpty(path))
return null;
// If the configured path starts with a home character
if (path!.StartsWith("~/") || path.StartsWith("~\\"))
{
string homeDirectory = PathTool.GetHomeDirectory();
path = path.Substring(2);
path = Path.Combine(homeDirectory, path);
return File.Exists(path) ? Path.GetFullPath(path) : null;
}
// Explicit location (absolute or relative path)
if (path.Contains("/") || path.Contains("\\"))
return File.Exists(path) ? Path.GetFullPath(path) : null;
// Check the runtime directory if no directory path is provided
string runtimeDir = PathTool.GetRuntimeDirectory();
if (!string.IsNullOrEmpty(runtimeDir))
{
string candidate = Path.Combine(runtimeDir, path);
if (File.Exists(candidate))
return candidate;
}
// Attempt to get the PATH variable for searching
string? pathEnv = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(pathEnv))
return null;
// Loop through all entries in PATH
var pathParts = pathEnv!.Split(Path.PathSeparator);
foreach (string dir in pathParts)
{
if (string.IsNullOrEmpty(dir))
continue;
string candidate = Path.Combine(dir, path);
if (File.Exists(candidate))
return candidate;
}
// All options failed
return null;
}
#endregion
#region Safe Directory Enumeration
/// <inheritdoc cref="Directory.GetDirectories(string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetDirectories(this string path)
{
try
{
return Directory.GetDirectories(path);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetDirectories(string, string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetDirectories(this string path, string searchPattern)
{
try
{
return Directory.GetDirectories(path, searchPattern);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetDirectories(string, string, SearchOption)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetDirectories(this string path, string searchPattern, SearchOption searchOption)
{
try
{
// Outside of Windows, linked directories are not followed
if (SkipLinkedDirectories(searchPattern, searchOption))
return [.. WalkWithoutLinkedDirectories(path, searchPattern, Directory.GetDirectories, Directory.GetDirectories)];
return Directory.GetDirectories(path, searchPattern, searchOption);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetFiles(string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetFiles(this string path)
{
try
{
return Directory.GetFiles(path);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetFiles(string, string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetFiles(this string path, string searchPattern)
{
try
{
return Directory.GetFiles(path, searchPattern);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetFiles(string, string, SearchOption)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetFiles(this string path, string searchPattern, SearchOption searchOption)
{
try
{
// Outside of Windows, linked directories are not followed
if (SkipLinkedDirectories(searchPattern, searchOption))
return [.. WalkWithoutLinkedDirectories(path, searchPattern, Directory.GetFiles, Directory.GetDirectories)];
return Directory.GetFiles(path, searchPattern, searchOption);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetFileSystemEntries(string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetFileSystemEntries(this string path)
{
try
{
return Directory.GetFileSystemEntries(path);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetDirectories(string, string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static string[] SafeGetFileSystemEntries(this string path, string searchPattern)
{
try
{
return Directory.GetFileSystemEntries(path, searchPattern);
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.GetDirectories(string, string, SearchOption)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static IEnumerable<string> SafeGetFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
{
try
{
var enumerable = Directory.GetFileSystemEntries(path, searchPattern);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
#if NET20 || NET35
/// <inheritdoc cref="Directory.GetDirectories(string)"/>
/// <remarks>Calls <see cref="SafeGetDirectories(string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateDirectories(this string path)
=> path.SafeGetDirectories();
/// <inheritdoc cref="Directory.GetDirectories(string, string)"/>
/// <remarks>Calls <see cref="SafeGetDirectories(string, string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern)
=> path.SafeGetDirectories(searchPattern);
/// <inheritdoc cref="Directory.GetDirectories(string, string, SearchOption)"/>
/// <remarks>Calls <see cref="SafeGetDirectories(string, string, SearchOption)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption)
=> path.SafeGetDirectories(searchPattern, searchOption);
/// <inheritdoc cref="Directory.GetFiles(string)"/>
/// <remarks>Calls <see cref="SafeGetFiles(string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFiles(this string path)
=> path.SafeGetFiles();
/// <inheritdoc cref="Directory.GetFiles(string, string)"/>
/// <remarks>Calls <see cref="SafeGetFiles(string, string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern)
=> path.SafeGetFiles(searchPattern);
/// <inheritdoc cref="Directory.GetFiles(string, string, SearchOption)"/>
/// <remarks>Calls <see cref="SafeGetFiles(string, string, SearchOption)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption)
=> path.SafeGetFiles(searchPattern, searchOption);
/// <inheritdoc cref="Directory.GetFileSystemEntries(string)"/>
/// <remarks>Calls <see cref="SafeGetFileSystemEntries(string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path)
=> path.SafeGetFileSystemEntries();
/// <inheritdoc cref="Directory.GetFileSystemEntries(string, string)"/>
/// <remarks>Calls <see cref="SafeGetFileSystemEntries(string, string)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern)
=> path.SafeGetFileSystemEntries(searchPattern);
/// <inheritdoc cref="Directory.GetFileSystemEntries(string, string)"/>
/// <remarks>Calls <see cref="SafeGetFileSystemEntries(string, string, SearchOption)"/> implementation</remarks>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
=> path.SafeGetFileSystemEntries(searchPattern, searchOption);
#elif NET40_OR_GREATER || NETSTANDARD2_0
/// <inheritdoc cref="Directory.EnumerateDirectories(string)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path)
{
try
{
var enumerable = Directory.EnumerateDirectories(path);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern)
{
try
{
var enumerable = Directory.EnumerateDirectories(path, searchPattern);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption)
{
try
{
// Outside of Windows, linked directories are not followed
if (SkipLinkedDirectories(searchPattern, searchOption))
return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateDirectories, SafeEnumerateDirectories);
var enumerable = Directory.EnumerateDirectories(path, searchPattern, searchOption);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path)
{
try
{
var enumerable = Directory.EnumerateFiles(path);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string, string)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern)
{
try
{
var enumerable = Directory.EnumerateFiles(path, searchPattern);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption)
{
try
{
// Outside of Windows, linked directories are not followed
if (SkipLinkedDirectories(searchPattern, searchOption))
return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateFiles, SafeEnumerateDirectories);
var enumerable = Directory.EnumerateFiles(path, searchPattern, searchOption);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path)
{
try
{
var enumerable = Directory.EnumerateFileSystemEntries(path);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern)
{
try
{
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
{
try
{
// Outside of Windows, linked directories are not followed
if (SkipLinkedDirectories(searchPattern, searchOption))
return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateFileSystemEntries, SafeEnumerateDirectories);
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, searchOption);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
#else
/// <inheritdoc cref="Directory.EnumerateDirectories(string)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path)
{
try
{
string searchPattern = "*";
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern)
{
try
{
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption)
{
try
{
// Outside of Windows, linked directories are not followed
if (SkipLinkedDirectories(searchPattern, searchOption))
return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateDirectories, SafeEnumerateDirectories);
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path)
{
try
{
string searchPattern = "*";
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string, string)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern)
{
try
{
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFiles(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption)
{
try
{
// Outside of Windows, linked directories are not followed
if (SkipLinkedDirectories(searchPattern, searchOption))
return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateFiles, SafeEnumerateDirectories);
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path)
{
try
{
string searchPattern = "*";
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern)
{
try
{
SearchOption searchOption = SearchOption.TopDirectoryOnly;
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string, SearchOption)"/>
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
{
try
{
// Outside of Windows, linked directories are not followed
if (SkipLinkedDirectories(searchPattern, searchOption))
return WalkWithoutLinkedDirectories(path, searchPattern, SafeEnumerateFileSystemEntries, SafeEnumerateDirectories);
var enumerationOptions = FromSearchOption(searchOption);
enumerationOptions.IgnoreInaccessible = true;
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions);
return enumerable.SafeEnumerate();
}
catch
{
return [];
}
}
/// <summary>Initializes a new instance of the <see cref="EnumerationOptions" /> class with the recommended default options.</summary>
/// <see href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/IO/EnumerationOptions.cs#L42"</remarks>
private static EnumerationOptions FromSearchOption(SearchOption searchOption)
{
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories))
throw new ArgumentOutOfRangeException(nameof(searchOption));
return searchOption == SearchOption.AllDirectories
? new EnumerationOptions { RecurseSubdirectories = true, MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = false }
: new EnumerationOptions { MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = false };
}
#endif
/// <summary>
/// Lists the entries of one directory that match a pattern
/// </summary>
private delegate IEnumerable<string> DirectoryListing(string directory, string searchPattern);
/// <summary>
/// Indicates if a path is a linked directory
/// </summary>
/// <param name="path">Path to check</param>
/// <returns>True if the path is a directory that carries FileAttributes.ReparsePoint, false otherwise</returns>
/// <remarks>
/// Returns false if the directory cannot be read, such as below a directory without
/// execute permission, so the directory is entered and fails like in the recursive search
/// </remarks>
private static bool IsLinkedDirectory(string path)
{
// Exists returns false instead of throwing if the directory cannot be read
var info = new DirectoryInfo(path);
if (!info.Exists)
return false;
#if NET20 || NET35
return (info.Attributes & FileAttributes.ReparsePoint) != 0;
#else
return info.Attributes.HasFlag(FileAttributes.ReparsePoint);
#endif
}
/// <summary>
/// Indicates if a search walks the directories itself to skip linked directories
/// </summary>
/// <param name="searchPattern">Pattern to match the entry names against</param>
/// <param name="searchOption">Whether to search the top directory only or all subdirectories</param>
/// <returns>True if the walk is used, false if the recursive search is used</returns>
private static bool SkipLinkedDirectories(string searchPattern, SearchOption searchOption)
{
// Only a search of all subdirectories enters subdirectories
if (searchOption != SearchOption.AllDirectories)
return false;
// Linked directories on discs are only read as links outside of Windows
if (SystemEnvironment.IsWindows())
return false;
// A directory part of the pattern is matched once from the top directory, but the walk would match it in each directory
if (!string.IsNullOrEmpty(Path.GetDirectoryName(searchPattern)))
return false;
return true;
}
/// <summary>
/// Walk a directory and all subdirectories without following linked directories
/// </summary>
/// <param name="path">Directory to search</param>
/// <param name="searchPattern">Pattern to match the entry names against</param>
/// <param name="getEntries">Lists the matching entries of one directory</param>
/// <param name="getDirectories">Lists the subdirectories of one directory</param>
/// <returns>The same entries in the same order as the recursive search, minus those under linked directories</returns>
/// <remarks>
/// A linked directory that points back into the tree, such as a UDF symlink whose
/// target reads as empty on Linux, makes the recursive search run without end
/// </remarks>
private static IEnumerable<string> WalkWithoutLinkedDirectories(string path, string searchPattern, DirectoryListing getEntries, DirectoryListing getDirectories)
{
var pending = new Queue<string>();
pending.Enqueue(path);
while (pending.Count > 0)
{
string directory = pending.Dequeue();
foreach (string entry in getEntries(directory, searchPattern))
{
yield return entry;
}
foreach (string subdirectory in getDirectories(directory, "*"))
{
if (!IsLinkedDirectory(subdirectory))
pending.Enqueue(subdirectory);
}
}
}
#endregion
}
}