PageRenderTime 39ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 1ms

/Code/Foundation/Windows/IO/FileSystemEnumerator.cs

https://github.com/DavidMoore/Foundation
C# | 497 lines | 387 code | 59 blank | 51 comment | 96 complexity | 0c225e538fedc74b96949ca332ba89f2 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics.Contracts;
  4. using System.IO;
  5. using System.Runtime.InteropServices;
  6. using System.Runtime.Versioning;
  7. using System.Security;
  8. using System.Security.Permissions;
  9. using System.Text;
  10. using Foundation.ExtensionMethods;
  11. namespace Foundation.Windows.IO
  12. {
  13. /// <summary>
  14. /// Enumerates the file system by searching a specified path
  15. /// that can contain wild cards, and additionally
  16. /// search within sub directories.
  17. /// </summary>
  18. /// <typeparam name="TSource">The type of result to return.</typeparam>
  19. public class FileSystemEnumerator<TSource> : Iterator<TSource>
  20. {
  21. const int stateInit = 1;
  22. const int stateSearchNextDir = 2;
  23. const int stateFindNextFile = 3;
  24. const int stateFinish = 4;
  25. readonly SearchResultHandler<TSource> resultHandler;
  26. readonly String fullPath;
  27. readonly String normalizedSearchPath;
  28. readonly int oldMode;
  29. readonly String searchCriteria;
  30. readonly SearchOption searchOption;
  31. readonly List<SearchData> searchStack;
  32. readonly String userPath;
  33. [SecurityCritical] SafeFindHandle safeFindHandle;
  34. bool isEmpty;
  35. bool needsParentPathDiscoveryDemand;
  36. SearchData searchData;
  37. // Input to this method should already be fullpath. This method will ensure that we append
  38. // the trailing slash only when appropriate and when thisDirOnly is specified append a "."
  39. // at the end of the path to indicate that the demand is only for the fullpath and not
  40. // everything underneath it.
  41. [SecuritySafeCritical]
  42. public FileSystemEnumerator(String path, String originalUserPath, String searchPattern, SearchOption searchOption, SearchResultHandler<TSource> resultHandler)
  43. {
  44. Contract.Requires(path != null);
  45. Contract.Requires(originalUserPath != null);
  46. Contract.Requires(searchPattern != null);
  47. Contract.Requires(searchOption == SearchOption.AllDirectories || searchOption == SearchOption.TopDirectoryOnly);
  48. Contract.Requires(resultHandler != null);
  49. oldMode = Win32Api.SetErrorMode(Win32Api.FailCriticalErrors);
  50. searchStack = new List<SearchData>();
  51. String normalizedSearchPattern = NormalizeSearchPattern(searchPattern);
  52. if( normalizedSearchPattern.Length == 0 )
  53. {
  54. isEmpty = true;
  55. }
  56. else
  57. {
  58. this.resultHandler = resultHandler;
  59. this.searchOption = searchOption;
  60. fullPath = PathHelperMethods.GetFullPathInternal(path);
  61. String fullSearchString = GetFullSearchString(fullPath, normalizedSearchPattern);
  62. normalizedSearchPath = Path.GetDirectoryName(fullSearchString);
  63. // permission demands
  64. var demandPaths = new String[2];
  65. // Any illegal chars such as *, ? will be caught by FileIOPermission.HasIllegalCharacters
  66. demandPaths[0] = GetDemandDir(fullPath, true);
  67. // For filters like foo\*.cs we need to verify if the directory foo is not denied access.
  68. // Do a demand on the combined path so that we can fail early in case of deny
  69. demandPaths[1] = GetDemandDir(normalizedSearchPath, true);
  70. new FileIOPermission(FileIOPermissionAccess.PathDiscovery, demandPaths).Demand();
  71. // normalize search criteria
  72. searchCriteria = GetNormalizedSearchCriteria(fullSearchString, normalizedSearchPath);
  73. // fix up user path
  74. String searchPatternDirName = Path.GetDirectoryName(normalizedSearchPattern);
  75. String userPathTemp = originalUserPath;
  76. if( searchPatternDirName != null && searchPatternDirName.Length != 0 )
  77. {
  78. userPathTemp = Path.Combine(userPathTemp, searchPatternDirName);
  79. }
  80. userPath = userPathTemp;
  81. searchData = new SearchData(normalizedSearchPath, userPath, searchOption);
  82. CommonInit();
  83. }
  84. }
  85. [SecuritySafeCritical]
  86. FileSystemEnumerator(String fullPath, String normalizedSearchPath, String searchCriteria, String userPath, SearchOption searchOption, SearchResultHandler<TSource> resultHandler)
  87. {
  88. this.fullPath = fullPath;
  89. this.normalizedSearchPath = normalizedSearchPath;
  90. this.searchCriteria = searchCriteria;
  91. this.resultHandler = resultHandler;
  92. this.userPath = userPath;
  93. this.searchOption = searchOption;
  94. searchStack = new List<SearchData>();
  95. if( searchCriteria != null )
  96. {
  97. // permission demands
  98. var demandPaths = new String[2];
  99. // Any illegal chars such as *, ? will be caught by FileIOPermission.HasIllegalCharacters
  100. demandPaths[0] = GetDemandDir(fullPath, true);
  101. // For filters like foo\*.cs we need to verify if the directory foo is not denied access.
  102. // Do a demand on the combined path so that we can fail early in case of deny
  103. demandPaths[1] = GetDemandDir(normalizedSearchPath, true);
  104. new FileIOPermission(FileIOPermissionAccess.PathDiscovery, demandPaths).Demand();
  105. searchData = new SearchData(normalizedSearchPath, userPath, searchOption);
  106. CommonInit();
  107. }
  108. else
  109. {
  110. isEmpty = true;
  111. }
  112. }
  113. [ResourceExposure(ResourceScope.None)]
  114. [ResourceConsumption(ResourceScope.None, ResourceScope.None)]
  115. internal static String GetDemandDir(string fullPath, bool thisDirOnly)
  116. {
  117. String demandPath;
  118. if( thisDirOnly )
  119. {
  120. if( fullPath.EndsWith(PathHelperMethods.DirectorySeparatorChar)
  121. || fullPath.EndsWith(PathHelperMethods.AltDirectorySeparatorChar) )
  122. demandPath = fullPath + '.';
  123. else
  124. demandPath = fullPath + Path.DirectorySeparatorChar + '.';
  125. }
  126. else
  127. {
  128. if( !(fullPath.EndsWith(Path.DirectorySeparatorChar)
  129. || fullPath.EndsWith(Path.AltDirectorySeparatorChar)) )
  130. demandPath = fullPath + Path.DirectorySeparatorChar;
  131. else
  132. demandPath = fullPath;
  133. }
  134. return demandPath;
  135. }
  136. [SecurityCritical]
  137. void CommonInit()
  138. {
  139. Contract.Assert(searchCriteria != null && searchData != null, "searchCriteria and searchData should be initialized");
  140. // Execute searchCriteria against the current directory
  141. String searchPath = searchData.fullPath + searchCriteria;
  142. var data = new FindData();
  143. // Open a Find handle
  144. safeFindHandle = Win32Api.IO.FindFirstFile(searchPath, data);
  145. if( safeFindHandle.IsInvalid )
  146. {
  147. int hr = Marshal.GetLastWin32Error();
  148. if( hr != Win32Error.ERROR_FILE_NOT_FOUND && hr != Win32Error.ERROR_NO_MORE_FILES )
  149. {
  150. HandleError(hr, searchData.fullPath);
  151. }
  152. else
  153. {
  154. // flag this as empty only if we're searching just top directory
  155. // Used in fast path for top directory only
  156. isEmpty = searchData.searchOptions == SearchOption.TopDirectoryOnly;
  157. }
  158. }
  159. // fast path for TopDirectoryOnly. If we have a result, go ahead and set it to
  160. // current. If empty, dispose handle.
  161. if( searchData.searchOptions == SearchOption.TopDirectoryOnly )
  162. {
  163. if( isEmpty )
  164. {
  165. safeFindHandle.Dispose();
  166. }
  167. else
  168. {
  169. SearchResult searchResult = CreateSearchResult(searchData, data);
  170. if( resultHandler.IsResultIncluded(searchResult) )
  171. {
  172. current = resultHandler.CreateObject(searchResult);
  173. }
  174. }
  175. }
  176. // for AllDirectories, we first recurse into dirs, so cleanup and add searchData
  177. // to the stack
  178. else
  179. {
  180. safeFindHandle.Dispose();
  181. searchStack.Add(searchData);
  182. }
  183. }
  184. [SecuritySafeCritical]
  185. protected override Iterator<TSource> Clone()
  186. {
  187. return new FileSystemEnumerator<TSource>(fullPath, normalizedSearchPath, searchCriteria, userPath, searchOption, resultHandler);
  188. }
  189. [SecuritySafeCritical]
  190. protected override void Dispose(bool disposing)
  191. {
  192. try
  193. {
  194. if( safeFindHandle != null )
  195. {
  196. safeFindHandle.Dispose();
  197. }
  198. }
  199. finally
  200. {
  201. Win32Api.SetErrorMode(oldMode);
  202. base.Dispose(disposing);
  203. }
  204. }
  205. [SecuritySafeCritical]
  206. public override bool MoveNext()
  207. {
  208. var data = new FindData();
  209. switch( state )
  210. {
  211. case stateInit:
  212. {
  213. if( isEmpty )
  214. {
  215. state = stateFinish;
  216. goto case stateFinish;
  217. }
  218. if( searchData.searchOptions == SearchOption.TopDirectoryOnly )
  219. {
  220. state = stateFindNextFile;
  221. if( current != null )
  222. {
  223. return true;
  224. }
  225. goto case stateFindNextFile;
  226. }
  227. state = stateSearchNextDir;
  228. goto case stateSearchNextDir;
  229. }
  230. case stateSearchNextDir:
  231. {
  232. Contract.Assert(searchData.searchOptions != SearchOption.TopDirectoryOnly, "should not reach this code path if searchOption == TopDirectoryOnly");
  233. // Traverse directory structure. We need to get '*'
  234. while( searchStack.Count > 0 )
  235. {
  236. searchData = searchStack[0];
  237. Contract.Assert((searchData.fullPath != null), "fullpath can't be null!");
  238. searchStack.RemoveAt(0);
  239. // Traverse the subdirs
  240. AddSearchableDirsToStack(searchData);
  241. // Execute searchCriteria against the current directory
  242. String searchPath = searchData.fullPath + searchCriteria;
  243. // Open a Find handle
  244. safeFindHandle = Win32Api.IO.FindFirstFile(searchPath, data);
  245. if( safeFindHandle.IsInvalid )
  246. {
  247. int hr = Marshal.GetLastWin32Error();
  248. if( hr == Win32Error.ERROR_ACCESS_DENIED || hr == Win32Error.ERROR_FILE_NOT_FOUND || hr == Win32Error.ERROR_NO_MORE_FILES || hr == Win32Error.ERROR_PATH_NOT_FOUND )
  249. continue;
  250. safeFindHandle.Dispose();
  251. HandleError(hr, searchData.fullPath);
  252. }
  253. state = stateFindNextFile;
  254. needsParentPathDiscoveryDemand = true;
  255. SearchResult searchResult = CreateSearchResult(searchData, data);
  256. if( resultHandler.IsResultIncluded(searchResult) )
  257. {
  258. if( needsParentPathDiscoveryDemand )
  259. {
  260. DoDemand(searchData.fullPath);
  261. needsParentPathDiscoveryDemand = false;
  262. }
  263. current = resultHandler.CreateObject(searchResult);
  264. return true;
  265. }
  266. goto case stateFindNextFile;
  267. }
  268. state = stateFinish;
  269. goto case stateFinish;
  270. }
  271. case stateFindNextFile:
  272. {
  273. if( searchData != null && safeFindHandle != null )
  274. {
  275. // Keep asking for more matching files/dirs, add it to the list
  276. while( Win32Api.IO.FindNextFile(safeFindHandle, data) )
  277. {
  278. SearchResult searchResult = CreateSearchResult(searchData, data);
  279. if( resultHandler.IsResultIncluded(searchResult) )
  280. {
  281. if( needsParentPathDiscoveryDemand )
  282. {
  283. DoDemand(searchData.fullPath);
  284. needsParentPathDiscoveryDemand = false;
  285. }
  286. current = resultHandler.CreateObject(searchResult);
  287. return true;
  288. }
  289. }
  290. // Make sure we quit with a sensible error.
  291. int hr = Marshal.GetLastWin32Error();
  292. if( safeFindHandle != null )
  293. safeFindHandle.Dispose();
  294. // ERROR_FILE_NOT_FOUND is valid here because if the top level
  295. // dir doen't contain any subdirs and matching files then
  296. // we will get here with this errorcode from the searchStack walk
  297. if( (hr != 0) && (hr != Win32Error.ERROR_NO_MORE_FILES)
  298. && (hr != Win32Error.ERROR_FILE_NOT_FOUND) )
  299. {
  300. HandleError(hr, searchData.fullPath);
  301. }
  302. }
  303. if( searchData.searchOptions == SearchOption.TopDirectoryOnly )
  304. {
  305. state = stateFinish;
  306. goto case stateFinish;
  307. }
  308. state = stateSearchNextDir;
  309. goto case stateSearchNextDir;
  310. }
  311. case stateFinish:
  312. {
  313. Dispose();
  314. break;
  315. }
  316. }
  317. return false;
  318. }
  319. [SecurityCritical]
  320. SearchResult CreateSearchResult(SearchData localSearchData, FindData findData)
  321. {
  322. String userPathFinal = PathHelperMethods.InternalCombine(localSearchData.userPath, findData.FileName);
  323. String fullPathFinal = PathHelperMethods.InternalCombine(localSearchData.fullPath, findData.FileName);
  324. return new SearchResult(fullPathFinal, userPathFinal, findData);
  325. }
  326. [SecurityCritical]
  327. void HandleError(int hr, String path)
  328. {
  329. Dispose();
  330. ErrorHelper.WinIoError(hr, path);
  331. }
  332. [SecurityCritical] // auto-generated
  333. void AddSearchableDirsToStack(SearchData localSearchData)
  334. {
  335. Contract.Requires(localSearchData != null);
  336. String searchPath = localSearchData.fullPath + "*";
  337. SafeFindHandle hnd = null;
  338. var data = new FindData();
  339. try
  340. {
  341. // Get all files and dirs
  342. hnd = Win32Api.IO.FindFirstFile(searchPath, data);
  343. if( hnd.IsInvalid )
  344. {
  345. int hr = Marshal.GetLastWin32Error();
  346. // This could happen if the dir doesn't contain any files.
  347. // Continue with the recursive search though, eventually
  348. // searchStack will become empty
  349. if( hr == Win32Error.ERROR_ACCESS_DENIED || hr == Win32Error.ERROR_FILE_NOT_FOUND || hr == Win32Error.ERROR_NO_MORE_FILES || hr == Win32Error.ERROR_PATH_NOT_FOUND )
  350. return;
  351. HandleError(hr, localSearchData.fullPath);
  352. }
  353. // Add subdirs to searchStack. Exempt ReparsePoints as appropriate
  354. int incr = 0;
  355. do
  356. {
  357. if( data.IsDir )
  358. {
  359. // FullPath
  360. var pathBuffer = new StringBuilder(localSearchData.fullPath);
  361. pathBuffer.Append(data.FileName);
  362. String tempFullPath = pathBuffer.ToString();
  363. // UserPath
  364. pathBuffer.Length = 0;
  365. pathBuffer.Append(localSearchData.userPath);
  366. pathBuffer.Append(data.FileName);
  367. SearchOption option = localSearchData.searchOptions;
  368. #if EXCLUDE_REPARSEPOINTS
  369. // Traverse reparse points depending on the searchoption specified
  370. if ((searchDataSubDir.searchOption == SearchOption.AllDirectories) && (0 != (data.dwFileAttributes & Win32Native.FILE_ATTRIBUTE_REPARSE_POINT)))
  371. option = SearchOption.TopDirectoryOnly;
  372. #endif
  373. // Setup search data for the sub directory and push it into the stack
  374. var searchDataSubDir = new SearchData(tempFullPath, pathBuffer.ToString(), option);
  375. searchStack.Insert(incr++, searchDataSubDir);
  376. }
  377. } while( Win32Api.IO.FindNextFile(hnd, data) );
  378. // We don't care about errors here
  379. }
  380. finally
  381. {
  382. if( hnd != null )
  383. hnd.Dispose();
  384. }
  385. }
  386. [SecurityCritical]
  387. internal static void DoDemand(String fullPath)
  388. {
  389. var demandPaths = new[] {GetDemandDir(fullPath, true)};
  390. new FileIOPermission(FileIOPermissionAccess.PathDiscovery, demandPaths).Demand();
  391. }
  392. static String NormalizeSearchPattern(String searchPattern)
  393. {
  394. Contract.Requires(searchPattern != null);
  395. // Win32 normalization trims only U+0020.
  396. String tempSearchPattern = searchPattern.TrimEnd(PathHelperMethods.TrimEndChars);
  397. // Make this corner case more useful, like dir
  398. if( tempSearchPattern.Equals(".") )
  399. {
  400. tempSearchPattern = "*";
  401. }
  402. PathHelperMethods.CheckSearchPattern(tempSearchPattern);
  403. return tempSearchPattern;
  404. }
  405. static String GetNormalizedSearchCriteria(String fullSearchString, String fullPathMod)
  406. {
  407. Contract.Requires(fullSearchString != null);
  408. Contract.Requires(fullPathMod != null);
  409. Contract.Requires(fullSearchString.Length >= fullPathMod.Length);
  410. String searchCriteria = null;
  411. char lastChar = fullPathMod[fullPathMod.Length - 1];
  412. if( PathHelperMethods.IsDirectorySeparator(lastChar) )
  413. {
  414. // Can happen if the path is C:\temp, in which case GetDirectoryName would return C:\
  415. searchCriteria = fullSearchString.Substring(fullPathMod.Length);
  416. }
  417. else
  418. {
  419. Contract.Assert(fullSearchString.Length > fullPathMod.Length);
  420. searchCriteria = fullSearchString.Substring(fullPathMod.Length + 1);
  421. }
  422. return searchCriteria;
  423. }
  424. static String GetFullSearchString(String fullPath, String searchPattern)
  425. {
  426. Contract.Requires(fullPath != null);
  427. Contract.Requires(searchPattern != null);
  428. String tempStr = PathHelperMethods.InternalCombine(fullPath, searchPattern);
  429. // If path ends in a trailing slash (\), append a * or we'll get a "Cannot find the file specified" exception
  430. char lastChar = tempStr[tempStr.Length - 1];
  431. if( PathHelperMethods.IsDirectorySeparator(lastChar) || lastChar == Path.VolumeSeparatorChar )
  432. {
  433. tempStr = tempStr + '*';
  434. }
  435. return tempStr;
  436. }
  437. }
  438. }