/* Copyright (C) 2008-2016 Peter Palotas, Jeffrey Jangli, Alexandr Normuradov
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Text.RegularExpressions;
namespace Alphaleonis.Win32.Filesystem
{
/// Class that retrieves file system entries (i.e. files and directories) using Win32 API FindFirst()/FindNext().
[SerializableAttribute]
internal sealed class FindFileSystemEntryInfo
{
private static readonly Regex WildcardMatchAll = new Regex(@"^(\*)+(\.\*+)+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // special case to recognize *.* or *.** etc
private Regex _nameFilter;
private string _searchPattern = Path.WildcardStarMatchAll;
public FindFileSystemEntryInfo(bool isFolder, KernelTransaction transaction, string path, string searchPattern, DirectoryEnumerationOptions options, Type typeOfT, PathFormat pathFormat)
{
Transaction = transaction;
OriginalInputPath = path;
InputPath = Path.GetExtendedLengthPathCore(transaction, path, pathFormat, GetFullPathOptions.RemoveTrailingDirectorySeparator | GetFullPathOptions.FullCheck);
IsRelativePath = !Path.IsPathRooted(OriginalInputPath, false);
// .NET behaviour.
SearchPattern = searchPattern.TrimEnd(Path.TrimEndChars);
FileSystemObjectType = null;
ContinueOnException = (options & DirectoryEnumerationOptions.ContinueOnException) != 0;
AsLongPath = (options & DirectoryEnumerationOptions.AsLongPath) != 0;
AsString = typeOfT == typeof(string);
AsFileSystemInfo = !AsString && (typeOfT == typeof(FileSystemInfo) || typeOfT.BaseType == typeof(FileSystemInfo));
FindExInfoLevel = NativeMethods.IsAtLeastWindows7 && (options & DirectoryEnumerationOptions.BasicSearch) != 0
? NativeMethods.FINDEX_INFO_LEVELS.Basic
: NativeMethods.FINDEX_INFO_LEVELS.Standard;
LargeCache = NativeMethods.IsAtLeastWindows7 && (options & DirectoryEnumerationOptions.LargeCache) != 0
? NativeMethods.FindExAdditionalFlags.LargeFetch
: NativeMethods.FindExAdditionalFlags.None;
IsDirectory = isFolder;
if (IsDirectory)
{
// Need files or folders to enumerate.
if ((options & DirectoryEnumerationOptions.FilesAndFolders) == 0)
options |= DirectoryEnumerationOptions.FilesAndFolders;
FileSystemObjectType = (options & DirectoryEnumerationOptions.FilesAndFolders) == DirectoryEnumerationOptions.FilesAndFolders
? (bool?) null
: (options & DirectoryEnumerationOptions.Folders) != 0;
Recursive = (options & DirectoryEnumerationOptions.Recursive) != 0;
SkipReparsePoints = (options & DirectoryEnumerationOptions.SkipReparsePoints) != 0;
}
}
private void ThrowPossibleException(uint lastError, string pathLp)
{
//Answer
switch (lastError)
{
case Win32Errors.ERROR_NO_MORE_FILES:
lastError = Win32Errors.NO_ERROR;
break;
case Win32Errors.ERROR_FILE_NOT_FOUND:
case Win32Errors.ERROR_PATH_NOT_FOUND:
// MSDN: .NET 3.5+: DirectoryNotFoundException: Path is invalid, such as referring to an unmapped drive.
// Directory.Delete()
lastError = IsDirectory ? (int) Win32Errors.ERROR_PATH_NOT_FOUND : Win32Errors.ERROR_FILE_NOT_FOUND;
break;
//case Win32Errors.ERROR_DIRECTORY:
// // MSDN: .NET 3.5+: IOException: path is a file name.
// // Directory.EnumerateDirectories()
// // Directory.EnumerateFiles()
// // Directory.EnumerateFileSystemEntries()
// // Directory.GetDirectories()
// // Directory.GetFiles()
// // Directory.GetFileSystemEntries()
// break;
//case Win32Errors.ERROR_ACCESS_DENIED:
// // MSDN: .NET 3.5+: UnauthorizedAccessException: The caller does not have the required permission.
// break;
}
if (lastError != Win32Errors.NO_ERROR)
NativeError.ThrowException(lastError, pathLp);
}
private SafeFindFileHandle FindFirstFile(string pathLp, out NativeMethods.WIN32_FIND_DATA win32FindData)
{
var searchOption = null != FileSystemObjectType && (bool)FileSystemObjectType
? NativeMethods.FINDEX_SEARCH_OPS.SearchLimitToDirectories
: NativeMethods.FINDEX_SEARCH_OPS.SearchNameMatch;
var handle = Transaction == null || !NativeMethods.IsAtLeastWindowsVista
// FindFirstFileEx() / FindFirstFileTransacted()
// In the ANSI version of this function, the name is limited to MAX_PATH characters.
// To extend this limit to 32,767 wide characters, call the Unicode version of the function and prepend "\\?\" to the path.
// 2013-01-13: MSDN confirms LongPath usage.
// A trailing backslash is not allowed.
? NativeMethods.FindFirstFileEx(Path.RemoveTrailingDirectorySeparator(pathLp, false), FindExInfoLevel, out win32FindData, searchOption, IntPtr.Zero, LargeCache)
: NativeMethods.FindFirstFileTransacted(Path.RemoveTrailingDirectorySeparator(pathLp, false), FindExInfoLevel, out win32FindData, searchOption, IntPtr.Zero, LargeCache, Transaction.SafeHandle);
var lastError = Marshal.GetLastWin32Error();
if (handle.IsInvalid)
{
handle.Close();
handle = null;
if (!ContinueOnException)
ThrowPossibleException((uint)lastError, pathLp);
}
return handle;
}
private T NewFileSystemEntryType(bool isFolder, NativeMethods.WIN32_FIND_DATA win32FindData, string fileName, string pathLp)
{
// Determine yield, e.g. don't return files when only folders are requested and vice versa.
if (null != FileSystemObjectType && (!(bool) FileSystemObjectType || !isFolder) && (!(bool) !FileSystemObjectType || isFolder))
return (T) (object) null;
// Determine yield.
if (null != fileName && !(_nameFilter == null || (_nameFilter != null && _nameFilter.IsMatch(fileName))))
return (T) (object) null;
var fullPathLp = (IsRelativePath ? OriginalInputPath + Path.DirectorySeparator : pathLp) + (!Utils.IsNullOrWhiteSpace(fileName) ? fileName : string.Empty);
// Return object instance FullPath property as string, optionally in long path format.
if (AsString)
return (T) (object) (AsLongPath ? fullPathLp : Path.GetRegularPathCore(fullPathLp, GetFullPathOptions.None, false));
// Make sure the requested file system object type is returned.
// null = Return files and directories.
// true = Return only directories.
// false = Return only files.
var fsei = new FileSystemEntryInfo(win32FindData) {FullPath = fullPathLp};
return AsFileSystemInfo
// Return object instance of type FileSystemInfo.
? (T) (object) (fsei.IsDirectory
? (FileSystemInfo)
new DirectoryInfo(Transaction, fsei.LongFullPath, PathFormat.LongFullPath) {EntryInfo = fsei}
: new FileInfo(Transaction, fsei.LongFullPath, PathFormat.LongFullPath) {EntryInfo = fsei})
// Return object instance of type FileSystemEntryInfo.
: (T) (object) fsei;
}
/// Get an enumerator that returns all of the file system objects that match the wildcards that are in any of the directories to be searched.
/// An instance: FileSystemEntryInfo, DirectoryInfo, FileInfo or string (full path).
[SecurityCritical]
public IEnumerable Enumerate()
{
// MSDN: Queue
// Represents a first-in, first-out collection of objects.
// The capacity of a Queue is the number of elements the Queue can hold.
// As elements are added to a Queue, the capacity is automatically increased as required through reallocation. The capacity can be decreased by calling TrimToSize.
// The growth factor is the number by which the current capacity is multiplied when a greater capacity is required. The growth factor is determined when the Queue is constructed.
// The capacity of the Queue will always increase by a minimum value, regardless of the growth factor; a growth factor of 1.0 will not prevent the Queue from increasing in size.
// If the size of the collection can be estimated, specifying the initial capacity eliminates the need to perform a number of resizing operations while adding elements to the Queue.
// This constructor is an O(n) operation, where n is capacity.
var dirs = new Queue(1000);
dirs.Enqueue(InputPath);
using (new NativeMethods.ChangeErrorMode(NativeMethods.ErrorMode.FailCriticalErrors))
while (dirs.Count > 0)
{
// Removes the object at the beginning of your Queue.
// The algorithmic complexity of this is O(1). It doesn't loop over elements.
var path = Path.AddTrailingDirectorySeparator(dirs.Dequeue(), false);
var pathLp = path + Path.WildcardStarMatchAll;
NativeMethods.WIN32_FIND_DATA win32FindData;
using (var handle = FindFirstFile(pathLp, out win32FindData))
{
if (handle == null && ContinueOnException)
continue;
do
{
var fileName = win32FindData.cFileName;
// Skip entries "." and ".."
if (fileName.Equals(Path.CurrentDirectoryPrefix, StringComparison.OrdinalIgnoreCase) ||
fileName.Equals(Path.ParentDirectoryPrefix, StringComparison.OrdinalIgnoreCase))
continue;
// Skip reparse points here to cleanly separate regular directories from links.
if (SkipReparsePoints && (win32FindData.dwFileAttributes & FileAttributes.ReparsePoint) != 0)
continue;
// If object is a folder, add it to the queue for later traversal.
var isFolder = (win32FindData.dwFileAttributes & FileAttributes.Directory) != 0;
if (Recursive && (win32FindData.dwFileAttributes & FileAttributes.Directory) != 0)
dirs.Enqueue(path + fileName);
var res = NewFileSystemEntryType(isFolder, win32FindData, fileName, path);
if (res == null)
continue;
yield return res;
} while (NativeMethods.FindNextFile(handle, out win32FindData));
var lastError = Marshal.GetLastWin32Error();
if (!ContinueOnException)
ThrowPossibleException((uint)lastError, pathLp);
}
}
}
/// Gets a specific file system object.
///
/// The return type is based on C# inference. Possible return types are:
/// - (full path), - ( or ), instance
/// or null in case an Exception is raised and is .
///
[SecurityCritical]
public T Get()
{
NativeMethods.WIN32_FIND_DATA win32FindData;
using (new NativeMethods.ChangeErrorMode(NativeMethods.ErrorMode.FailCriticalErrors))
using (var handle = FindFirstFile(InputPath, out win32FindData))
return handle == null
? (T)(object)null
: NewFileSystemEntryType((win32FindData.dwFileAttributes & FileAttributes.Directory) != 0, win32FindData, null, InputPath);
}
/// Gets or sets the ability to return the object as a instance.
/// returns the object as a instance.
public bool AsFileSystemInfo { get; internal set; }
/// Gets or sets the ability to return the full path in long full path format.
/// returns the full path in long full path format, returns the full path in regular path format.
public bool AsLongPath { get; internal set; }
/// Gets or sets the ability to return the object instance as a .
/// returns the full path of the object as a
public bool AsString { get; internal set; }
/// Gets the value indicating which to use.
public NativeMethods.FINDEX_INFO_LEVELS FindExInfoLevel { get; internal set; }
/// Gets or sets the ability to skip on access errors.
/// suppress any Exception that might be thrown as a result from a failure, such as ACLs protected directories or non-accessible reparse points.
public bool ContinueOnException { get; internal set; }
/// Gets the file system object type.
///
/// = Return files and directories.
/// = Return only directories.
/// = Return only files.
///
public bool? FileSystemObjectType { get; set; }
/// Gets or sets if the path is an absolute or relative path.
/// Gets a value indicating whether the specified path string contains absolute or relative path information.
public bool IsRelativePath { get; set; }
/// Gets or sets the initial path to the folder.
/// The initial path to the file or folder in long path format.
public string OriginalInputPath { get; internal set; }
/// Gets or sets the path to the folder.
/// The path to the file or folder in long path format.
public string InputPath { get; internal set; }
/// Gets or sets a value indicating which to use.
/// indicates a folder object, indicates a file object.
public bool IsDirectory { get; internal set; }
/// Gets the value indicating which to use.
public NativeMethods.FindExAdditionalFlags LargeCache { get; internal set; }
/// Specifies whether the search should include only the current directory or should include all subdirectories.
/// to all subdirectories.
public bool Recursive { get; internal set; }
/// Search for file system object-name using a pattern.
/// The path which has wildcard characters, for example, an asterisk () or a question mark ().
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
public string SearchPattern
{
get { return _searchPattern; }
internal set
{
if (null == value)
throw new ArgumentNullException("SearchPattern");
_searchPattern = value;
_nameFilter = _searchPattern == Path.WildcardStarMatchAll || WildcardMatchAll.IsMatch(_searchPattern)
? null
: new Regex(string.Format(CultureInfo.CurrentCulture, "^{0}$", Regex.Escape(_searchPattern).Replace(@"\*", ".*").Replace(@"\?", ".")), RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
}
/// skips ReparsePoints, will follow ReparsePoints.
public bool SkipReparsePoints { get; internal set; }
/// Get or sets the KernelTransaction instance.
/// The transaction.
public KernelTransaction Transaction { get; internal set; }
}
}