You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

388 lines
18 KiB

  1. /* Copyright (C) 2008-2016 Peter Palotas, Jeffrey Jangli, Alexandr Normuradov
  2. *
  3. * Permission is hereby granted, free of charge, to any person obtaining a copy
  4. * of this software and associated documentation files (the "Software"), to deal
  5. * in the Software without restriction, including without limitation the rights
  6. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7. * copies of the Software, and to permit persons to whom the Software is
  8. * furnished to do so, subject to the following conditions:
  9. *
  10. * The above copyright notice and this permission notice shall be included in
  11. * all copies or substantial portions of the Software.
  12. *
  13. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19. * THE SOFTWARE.
  20. */
  21. using System;
  22. using System.Collections.Generic;
  23. using System.Diagnostics.CodeAnalysis;
  24. using System.Globalization;
  25. using System.IO;
  26. using System.Runtime.InteropServices;
  27. using System.Security;
  28. using System.Text.RegularExpressions;
  29. namespace Alphaleonis.Win32.Filesystem
  30. {
  31. /// <summary>Class that retrieves file system entries (i.e. files and directories) using Win32 API FindFirst()/FindNext().</summary>
  32. [SerializableAttribute]
  33. internal sealed class FindFileSystemEntryInfo
  34. {
  35. private static readonly Regex WildcardMatchAll = new Regex(@"^(\*)+(\.\*+)+$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // special case to recognize *.* or *.** etc
  36. private Regex _nameFilter;
  37. private string _searchPattern = Path.WildcardStarMatchAll;
  38. public FindFileSystemEntryInfo(bool isFolder, KernelTransaction transaction, string path, string searchPattern, DirectoryEnumerationOptions options, Type typeOfT, PathFormat pathFormat)
  39. {
  40. Transaction = transaction;
  41. OriginalInputPath = path;
  42. InputPath = Path.GetExtendedLengthPathCore(transaction, path, pathFormat, GetFullPathOptions.RemoveTrailingDirectorySeparator | GetFullPathOptions.FullCheck);
  43. IsRelativePath = !Path.IsPathRooted(OriginalInputPath, false);
  44. // .NET behaviour.
  45. SearchPattern = searchPattern.TrimEnd(Path.TrimEndChars);
  46. FileSystemObjectType = null;
  47. ContinueOnException = (options & DirectoryEnumerationOptions.ContinueOnException) != 0;
  48. AsLongPath = (options & DirectoryEnumerationOptions.AsLongPath) != 0;
  49. AsString = typeOfT == typeof(string);
  50. AsFileSystemInfo = !AsString && (typeOfT == typeof(FileSystemInfo) || typeOfT.BaseType == typeof(FileSystemInfo));
  51. FindExInfoLevel = NativeMethods.IsAtLeastWindows7 && (options & DirectoryEnumerationOptions.BasicSearch) != 0
  52. ? NativeMethods.FINDEX_INFO_LEVELS.Basic
  53. : NativeMethods.FINDEX_INFO_LEVELS.Standard;
  54. LargeCache = NativeMethods.IsAtLeastWindows7 && (options & DirectoryEnumerationOptions.LargeCache) != 0
  55. ? NativeMethods.FindExAdditionalFlags.LargeFetch
  56. : NativeMethods.FindExAdditionalFlags.None;
  57. IsDirectory = isFolder;
  58. if (IsDirectory)
  59. {
  60. // Need files or folders to enumerate.
  61. if ((options & DirectoryEnumerationOptions.FilesAndFolders) == 0)
  62. options |= DirectoryEnumerationOptions.FilesAndFolders;
  63. FileSystemObjectType = (options & DirectoryEnumerationOptions.FilesAndFolders) == DirectoryEnumerationOptions.FilesAndFolders
  64. ? (bool?) null
  65. : (options & DirectoryEnumerationOptions.Folders) != 0;
  66. Recursive = (options & DirectoryEnumerationOptions.Recursive) != 0;
  67. SkipReparsePoints = (options & DirectoryEnumerationOptions.SkipReparsePoints) != 0;
  68. }
  69. }
  70. private void ThrowPossibleException(uint lastError, string pathLp)
  71. {
  72. //Answer
  73. switch (lastError)
  74. {
  75. case Win32Errors.ERROR_NO_MORE_FILES:
  76. lastError = Win32Errors.NO_ERROR;
  77. break;
  78. case Win32Errors.ERROR_FILE_NOT_FOUND:
  79. case Win32Errors.ERROR_PATH_NOT_FOUND:
  80. // MSDN: .NET 3.5+: DirectoryNotFoundException: Path is invalid, such as referring to an unmapped drive.
  81. // Directory.Delete()
  82. lastError = IsDirectory ? (int) Win32Errors.ERROR_PATH_NOT_FOUND : Win32Errors.ERROR_FILE_NOT_FOUND;
  83. break;
  84. //case Win32Errors.ERROR_DIRECTORY:
  85. // // MSDN: .NET 3.5+: IOException: path is a file name.
  86. // // Directory.EnumerateDirectories()
  87. // // Directory.EnumerateFiles()
  88. // // Directory.EnumerateFileSystemEntries()
  89. // // Directory.GetDirectories()
  90. // // Directory.GetFiles()
  91. // // Directory.GetFileSystemEntries()
  92. // break;
  93. //case Win32Errors.ERROR_ACCESS_DENIED:
  94. // // MSDN: .NET 3.5+: UnauthorizedAccessException: The caller does not have the required permission.
  95. // break;
  96. }
  97. if (lastError != Win32Errors.NO_ERROR)
  98. NativeError.ThrowException(lastError, pathLp);
  99. }
  100. private SafeFindFileHandle FindFirstFile(string pathLp, out NativeMethods.WIN32_FIND_DATA win32FindData)
  101. {
  102. var searchOption = null != FileSystemObjectType && (bool)FileSystemObjectType
  103. ? NativeMethods.FINDEX_SEARCH_OPS.SearchLimitToDirectories
  104. : NativeMethods.FINDEX_SEARCH_OPS.SearchNameMatch;
  105. var handle = Transaction == null || !NativeMethods.IsAtLeastWindowsVista
  106. // FindFirstFileEx() / FindFirstFileTransacted()
  107. // In the ANSI version of this function, the name is limited to MAX_PATH characters.
  108. // To extend this limit to 32,767 wide characters, call the Unicode version of the function and prepend "\\?\" to the path.
  109. // 2013-01-13: MSDN confirms LongPath usage.
  110. // A trailing backslash is not allowed.
  111. ? NativeMethods.FindFirstFileEx(Path.RemoveTrailingDirectorySeparator(pathLp, false), FindExInfoLevel, out win32FindData, searchOption, IntPtr.Zero, LargeCache)
  112. : NativeMethods.FindFirstFileTransacted(Path.RemoveTrailingDirectorySeparator(pathLp, false), FindExInfoLevel, out win32FindData, searchOption, IntPtr.Zero, LargeCache, Transaction.SafeHandle);
  113. var lastError = Marshal.GetLastWin32Error();
  114. if (handle.IsInvalid)
  115. {
  116. handle.Close();
  117. handle = null;
  118. if (!ContinueOnException)
  119. ThrowPossibleException((uint)lastError, pathLp);
  120. }
  121. return handle;
  122. }
  123. private T NewFileSystemEntryType<T>(bool isFolder, NativeMethods.WIN32_FIND_DATA win32FindData, string fileName, string pathLp)
  124. {
  125. // Determine yield, e.g. don't return files when only folders are requested and vice versa.
  126. if (null != FileSystemObjectType && (!(bool) FileSystemObjectType || !isFolder) && (!(bool) !FileSystemObjectType || isFolder))
  127. return (T) (object) null;
  128. // Determine yield.
  129. if (null != fileName && !(_nameFilter == null || (_nameFilter != null && _nameFilter.IsMatch(fileName))))
  130. return (T) (object) null;
  131. var fullPathLp = (IsRelativePath ? OriginalInputPath + Path.DirectorySeparator : pathLp) + (!Utils.IsNullOrWhiteSpace(fileName) ? fileName : string.Empty);
  132. // Return object instance FullPath property as string, optionally in long path format.
  133. if (AsString)
  134. return (T) (object) (AsLongPath ? fullPathLp : Path.GetRegularPathCore(fullPathLp, GetFullPathOptions.None, false));
  135. // Make sure the requested file system object type is returned.
  136. // null = Return files and directories.
  137. // true = Return only directories.
  138. // false = Return only files.
  139. var fsei = new FileSystemEntryInfo(win32FindData) {FullPath = fullPathLp};
  140. return AsFileSystemInfo
  141. // Return object instance of type FileSystemInfo.
  142. ? (T) (object) (fsei.IsDirectory
  143. ? (FileSystemInfo)
  144. new DirectoryInfo(Transaction, fsei.LongFullPath, PathFormat.LongFullPath) {EntryInfo = fsei}
  145. : new FileInfo(Transaction, fsei.LongFullPath, PathFormat.LongFullPath) {EntryInfo = fsei})
  146. // Return object instance of type FileSystemEntryInfo.
  147. : (T) (object) fsei;
  148. }
  149. /// <summary>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.</summary>
  150. /// <returns>An <see cref="IEnumerable{T}"/> instance: FileSystemEntryInfo, DirectoryInfo, FileInfo or string (full path).</returns>
  151. [SecurityCritical]
  152. public IEnumerable<T> Enumerate<T>()
  153. {
  154. // MSDN: Queue
  155. // Represents a first-in, first-out collection of objects.
  156. // The capacity of a Queue is the number of elements the Queue can hold.
  157. // As elements are added to a Queue, the capacity is automatically increased as required through reallocation. The capacity can be decreased by calling TrimToSize.
  158. // 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.
  159. // 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.
  160. // 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.
  161. // This constructor is an O(n) operation, where n is capacity.
  162. var dirs = new Queue<string>(1000);
  163. dirs.Enqueue(InputPath);
  164. using (new NativeMethods.ChangeErrorMode(NativeMethods.ErrorMode.FailCriticalErrors))
  165. while (dirs.Count > 0)
  166. {
  167. // Removes the object at the beginning of your Queue.
  168. // The algorithmic complexity of this is O(1). It doesn't loop over elements.
  169. var path = Path.AddTrailingDirectorySeparator(dirs.Dequeue(), false);
  170. var pathLp = path + Path.WildcardStarMatchAll;
  171. NativeMethods.WIN32_FIND_DATA win32FindData;
  172. using (var handle = FindFirstFile(pathLp, out win32FindData))
  173. {
  174. if (handle == null && ContinueOnException)
  175. continue;
  176. do
  177. {
  178. var fileName = win32FindData.cFileName;
  179. // Skip entries "." and ".."
  180. if (fileName.Equals(Path.CurrentDirectoryPrefix, StringComparison.OrdinalIgnoreCase) ||
  181. fileName.Equals(Path.ParentDirectoryPrefix, StringComparison.OrdinalIgnoreCase))
  182. continue;
  183. // Skip reparse points here to cleanly separate regular directories from links.
  184. if (SkipReparsePoints && (win32FindData.dwFileAttributes & FileAttributes.ReparsePoint) != 0)
  185. continue;
  186. // If object is a folder, add it to the queue for later traversal.
  187. var isFolder = (win32FindData.dwFileAttributes & FileAttributes.Directory) != 0;
  188. if (Recursive && (win32FindData.dwFileAttributes & FileAttributes.Directory) != 0)
  189. dirs.Enqueue(path + fileName);
  190. var res = NewFileSystemEntryType<T>(isFolder, win32FindData, fileName, path);
  191. if (res == null)
  192. continue;
  193. yield return res;
  194. } while (NativeMethods.FindNextFile(handle, out win32FindData));
  195. var lastError = Marshal.GetLastWin32Error();
  196. if (!ContinueOnException)
  197. ThrowPossibleException((uint)lastError, pathLp);
  198. }
  199. }
  200. }
  201. /// <summary>Gets a specific file system object.</summary>
  202. /// <returns>
  203. /// <para>The return type is based on C# inference. Possible return types are:</para>
  204. /// <para> <see cref="string"/>- (full path), <see cref="FileSystemInfo"/>- (<see cref="DirectoryInfo"/> or <see cref="FileInfo"/>), <see cref="FileSystemEntryInfo"/> instance</para>
  205. /// <para>or null in case an Exception is raised and <see cref="ContinueOnException"/> is <see langword="true"/>.</para>
  206. /// </returns>
  207. [SecurityCritical]
  208. public T Get<T>()
  209. {
  210. NativeMethods.WIN32_FIND_DATA win32FindData;
  211. using (new NativeMethods.ChangeErrorMode(NativeMethods.ErrorMode.FailCriticalErrors))
  212. using (var handle = FindFirstFile(InputPath, out win32FindData))
  213. return handle == null
  214. ? (T)(object)null
  215. : NewFileSystemEntryType<T>((win32FindData.dwFileAttributes & FileAttributes.Directory) != 0, win32FindData, null, InputPath);
  216. }
  217. /// <summary>Gets or sets the ability to return the object as a <see cref="FileSystemInfo"/> instance.</summary>
  218. /// <value><see langword="true"/> returns the object as a <see cref="FileSystemInfo"/> instance.</value>
  219. public bool AsFileSystemInfo { get; internal set; }
  220. /// <summary>Gets or sets the ability to return the full path in long full path format.</summary>
  221. /// <value><see langword="true"/> returns the full path in long full path format, <see langword="false"/> returns the full path in regular path format.</value>
  222. public bool AsLongPath { get; internal set; }
  223. /// <summary>Gets or sets the ability to return the object instance as a <see cref="string"/>.</summary>
  224. /// <value><see langword="true"/> returns the full path of the object as a <see cref="string"/></value>
  225. public bool AsString { get; internal set; }
  226. /// <summary>Gets the value indicating which <see cref="NativeMethods.FINDEX_INFO_LEVELS"/> to use.</summary>
  227. public NativeMethods.FINDEX_INFO_LEVELS FindExInfoLevel { get; internal set; }
  228. /// <summary>Gets or sets the ability to skip on access errors.</summary>
  229. /// <value><see langword="true"/> suppress any Exception that might be thrown as a result from a failure, such as ACLs protected directories or non-accessible reparse points.</value>
  230. public bool ContinueOnException { get; internal set; }
  231. /// <summary>Gets the file system object type.</summary>
  232. /// <value>
  233. /// <see langword="null"/> = Return files and directories.
  234. /// <see langword="true"/> = Return only directories.
  235. /// <see langword="false"/> = Return only files.
  236. /// </value>
  237. public bool? FileSystemObjectType { get; set; }
  238. /// <summary>Gets or sets if the path is an absolute or relative path.</summary>
  239. /// <value>Gets a value indicating whether the specified path string contains absolute or relative path information.</value>
  240. public bool IsRelativePath { get; set; }
  241. /// <summary>Gets or sets the initial path to the folder.</summary>
  242. /// <value>The initial path to the file or folder in long path format.</value>
  243. public string OriginalInputPath { get; internal set; }
  244. /// <summary>Gets or sets the path to the folder.</summary>
  245. /// <value>The path to the file or folder in long path format.</value>
  246. public string InputPath { get; internal set; }
  247. /// <summary>Gets or sets a value indicating which <see cref="NativeMethods.FINDEX_INFO_LEVELS"/> to use.</summary>
  248. /// <value><see langword="true"/> indicates a folder object, <see langword="false"/> indicates a file object.</value>
  249. public bool IsDirectory { get; internal set; }
  250. /// <summary>Gets the value indicating which <see cref="NativeMethods.FindExAdditionalFlags"/> to use.</summary>
  251. public NativeMethods.FindExAdditionalFlags LargeCache { get; internal set; }
  252. /// <summary>Specifies whether the search should include only the current directory or should include all subdirectories.</summary>
  253. /// <value><see langword="true"/> to all subdirectories.</value>
  254. public bool Recursive { get; internal set; }
  255. /// <summary>Search for file system object-name using a pattern.</summary>
  256. /// <value>The path which has wildcard characters, for example, an asterisk (<see cref="Path.WildcardStarMatchAll"/>) or a question mark (<see cref="Path.WildcardQuestion"/>).</value>
  257. [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
  258. public string SearchPattern
  259. {
  260. get { return _searchPattern; }
  261. internal set
  262. {
  263. if (null == value)
  264. throw new ArgumentNullException("SearchPattern");
  265. _searchPattern = value;
  266. _nameFilter = _searchPattern == Path.WildcardStarMatchAll || WildcardMatchAll.IsMatch(_searchPattern)
  267. ? null
  268. : new Regex(string.Format(CultureInfo.CurrentCulture, "^{0}$", Regex.Escape(_searchPattern).Replace(@"\*", ".*").Replace(@"\?", ".")), RegexOptions.IgnoreCase | RegexOptions.Compiled);
  269. }
  270. }
  271. /// <summary><see langword="true"/> skips ReparsePoints, <see langword="false"/> will follow ReparsePoints.</summary>
  272. public bool SkipReparsePoints { get; internal set; }
  273. /// <summary>Get or sets the KernelTransaction instance.</summary>
  274. /// <value>The transaction.</value>
  275. public KernelTransaction Transaction { get; internal set; }
  276. }
  277. }