mirror of
https://github.com/SabreTools/SabreTools.IO.git
synced 2026-11-15 16:41:58 +00:00
* 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>
1065 lines
42 KiB
C#
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
|
|
}
|
|
}
|