1 // Copyright (c) Microsoft. All rights reserved. 2 // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 4 using System; 5 using System.IO; 6 using System.Text; 7 using System.Diagnostics; 8 using System.Text.RegularExpressions; 9 using System.Threading; 10 using System.Globalization; 11 12 namespace Microsoft.Build.BuildEngine.Shared 13 { 14 /// <summary> 15 /// Functions for matching file names with patterns. 16 /// </summary> 17 /// <owner>JomoF</owner> 18 internal static class FileMatcher 19 { 20 private const string recursiveDirectoryMatch = "**"; 21 private const string dotdot = ".."; 22 private static readonly string directorySeparator = new string(Path.DirectorySeparatorChar,1); 23 private static readonly string altDirectorySeparator = new string(Path.AltDirectorySeparatorChar,1); 24 25 private static readonly char[] wildcardCharacters = { '*', '?' }; 26 internal static readonly char[] directorySeparatorCharacters = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; 27 private static readonly GetFileSystemEntries defaultGetFileSystemEntries = new GetFileSystemEntries(GetAccessibleFileSystemEntries); 28 private static readonly DirectoryExists defaultDirectoryExists = new DirectoryExists(Directory.Exists); 29 30 /// <summary> 31 /// The type of entity that GetFileSystemEntries should return. 32 /// </summary> 33 internal enum FileSystemEntity 34 { 35 Files, 36 Directories, 37 FilesAndDirectories 38 }; 39 40 /// <summary> 41 /// Delegate defines the GetFileSystemEntries signature that GetLongPathName uses 42 /// to enumerate directories on the file system. 43 /// </summary> 44 /// <param name="entityType">Files, Directories, or Files and Directories</param> 45 /// <param name="path">The path to search.</param> 46 /// <param name="pattern">The file pattern.</param> 47 /// <param name="projectDirectory"></param> 48 /// <param name="stripProjectDirectory"></param> 49 /// <returns>The array of filesystem entries.</returns> GetFileSystemEntries(FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory)50 internal delegate string[] GetFileSystemEntries(FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory); 51 52 /// <summary> 53 /// Returns true if the directory exists and is not a file, otherwise false. 54 /// </summary> 55 /// <param name="path">The path to check</param> 56 /// <returns>True if the directory exists.</returns> DirectoryExists(string path)57 internal delegate bool DirectoryExists(string path); 58 59 60 /// <summary> 61 /// Determines whether the given path has any wild card characters. 62 /// </summary> 63 /// <param name="filespec"></param> 64 /// <returns></returns> HasWildcards(string filespec)65 internal static bool HasWildcards(string filespec) 66 { 67 return -1 != filespec.IndexOfAny(wildcardCharacters); 68 } 69 70 /// <summary> 71 /// Get the files and\or folders specified by the given path and pattern. 72 /// </summary> 73 /// <param name="entityType">Whether Files, Directories or both.</param> 74 /// <param name="path">The path to search.</param> 75 /// <param name="pattern">The pattern to search.</param> 76 /// <param name="projectDirectory">The directory for the project within which the call is made</param> 77 /// <param name="stripProjectDirectory">If true the project directory should be stripped</param> 78 /// <returns></returns> GetAccessibleFileSystemEntries(FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory)79 private static string[] GetAccessibleFileSystemEntries(FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory) 80 { 81 string[] files = null; 82 switch (entityType) 83 { 84 case FileSystemEntity.Files: files = GetAccessibleFiles(path, pattern, projectDirectory, stripProjectDirectory); break; 85 case FileSystemEntity.Directories: files = GetAccessibleDirectories(path, pattern); break; 86 case FileSystemEntity.FilesAndDirectories: files = GetAccessibleFilesAndDirectories(path, pattern); break; 87 default: 88 ErrorUtilities.VerifyThrow(false, "Unexpected filesystem entity type."); 89 break; 90 } 91 92 return files; 93 } 94 95 /// <summary> 96 /// Returns an array of file system entries matching the specified search criteria. Inaccessible or non-existent file 97 /// system entries are skipped. 98 /// </summary> 99 /// <owner>SumedhK,JomoF</owner> 100 /// <param name="path"></param> 101 /// <param name="pattern"></param> 102 /// <returns>Array of matching file system entries (can be empty).</returns> GetAccessibleFilesAndDirectories(string path, string pattern)103 private static string[] GetAccessibleFilesAndDirectories(string path, string pattern) 104 { 105 string[] entries = null; 106 107 if (Directory.Exists(path)) 108 { 109 try 110 { 111 entries = Directory.GetFileSystemEntries(path, pattern); 112 } 113 // for OS security 114 catch (UnauthorizedAccessException) 115 { 116 // do nothing 117 } 118 // for code access security 119 catch (System.Security.SecurityException) 120 { 121 // do nothing 122 } 123 } 124 125 if (entries == null) 126 { 127 entries = new string[0]; 128 } 129 130 return entries; 131 } 132 133 /// <summary> 134 /// Same as Directory.GetFiles(...) except that files that 135 /// aren't accessible are skipped instead of throwing an exception. 136 /// 137 /// Other exceptions are passed through. 138 /// </summary> 139 /// <param name="path">The path.</param> 140 /// <param name="filespec">The pattern.</param> 141 /// <param name="projectDirectory">The project directory</param> 142 /// <param name="stripProjectDirectory"></param> 143 /// <returns>Files that can be accessed.</returns> GetAccessibleFiles( string path, string filespec, string projectDirectory, bool stripProjectDirectory )144 private static string[] GetAccessibleFiles 145 ( 146 string path, 147 string filespec, // can be null 148 string projectDirectory, 149 bool stripProjectDirectory 150 ) 151 { 152 try 153 { 154 // look in current directory if no path specified 155 string dir = ((path.Length == 0) ? ".\\" : path); 156 157 // get all files in specified directory, unless a file-spec has been provided 158 string[] files = (filespec == null) 159 ? Directory.GetFiles(dir) 160 : Directory.GetFiles(dir, filespec); 161 162 // If the Item is based on a relative path we need to strip 163 // the current directory from the front 164 if (stripProjectDirectory) 165 { 166 RemoveProjectDirectory(files, projectDirectory); 167 } 168 // Files in the current directory are coming back with a ".\" 169 // prepended to them. We need to remove this; it breaks the 170 // IDE, which expects just the filename if it is in the current 171 // directory. But only do this if the original path requested 172 // didn't itself contain a ".\". 173 else if (!path.StartsWith(".\\", StringComparison.Ordinal)) 174 { 175 RemoveInitialDotSlash(files); 176 } 177 178 return files; 179 } 180 catch (System.Security.SecurityException) 181 { 182 // For code access security. 183 return new string[0]; 184 } 185 catch (System.UnauthorizedAccessException) 186 { 187 // For OS security. 188 return new string[0]; 189 } 190 } 191 192 /// <summary> 193 /// Same as Directory.GetDirectories(...) except that files that 194 /// aren't accessible are skipped instead of throwing an exception. 195 /// 196 /// Other exceptions are passed through. 197 /// </summary> 198 /// <param name="path">The path.</param> 199 /// <param name="pattern">Pattern to match</param> 200 /// <returns>Accessible directories.</returns> GetAccessibleDirectories( string path, string pattern )201 private static string[] GetAccessibleDirectories 202 ( 203 string path, 204 string pattern 205 ) 206 { 207 try 208 { 209 string[] directories = null; 210 211 if (pattern == null) 212 { 213 directories = Directory.GetDirectories((path.Length == 0) ? ".\\" : path); 214 } 215 else 216 { 217 directories = Directory.GetDirectories((path.Length == 0) ? ".\\" : path, pattern); 218 } 219 220 // Subdirectories in the current directory are coming back with a ".\" 221 // prepended to them. We need to remove this; it breaks the 222 // IDE, which expects just the filename if it is in the current 223 // directory. But only do this if the original path requested 224 // didn't itself contain a ".\". 225 if (!path.StartsWith(".\\", StringComparison.Ordinal)) 226 { 227 RemoveInitialDotSlash(directories); 228 } 229 230 return directories; 231 } 232 catch (System.Security.SecurityException) 233 { 234 // For code access security. 235 return new string[0]; 236 } 237 catch (System.UnauthorizedAccessException) 238 { 239 // For OS security. 240 return new string[0]; 241 } 242 } 243 244 /// <summary> 245 /// Given a path name, get its long version. 246 /// </summary> 247 /// <param name="path">The short path.</param> 248 /// <param name="getFileSystemEntries">Delegate.</param> 249 /// <returns>The long path.</returns> GetLongPathName( string path, GetFileSystemEntries getFileSystemEntries )250 internal static string GetLongPathName 251 ( 252 string path, 253 GetFileSystemEntries getFileSystemEntries 254 ) 255 { 256 257 if (path.IndexOf("~", StringComparison.Ordinal) == -1) 258 { 259 // A path with no '~' must not be a short name. 260 return path; 261 } 262 263 ErrorUtilities.VerifyThrow(!HasWildcards(path), 264 "GetLongPathName does not handle wildcards and was passed '{0}'.", path); 265 266 string[] parts = path.Split(directorySeparatorCharacters); 267 string pathRoot; 268 int startingElement=0; 269 270 bool isUnc = path.StartsWith(directorySeparator + directorySeparator, StringComparison.Ordinal); 271 if (isUnc) 272 { 273 pathRoot = directorySeparator + directorySeparator; 274 pathRoot += parts[2]; 275 pathRoot += directorySeparator; 276 pathRoot += parts[3]; 277 pathRoot += directorySeparator; 278 startingElement = 4; 279 } 280 else 281 { 282 // Is it relative? 283 if (path.Length>2 && path[1] == ':') 284 { 285 // Not relative 286 pathRoot = parts[0] + directorySeparator; 287 startingElement = 1; 288 } 289 else 290 { 291 // Relative 292 pathRoot = String.Empty; 293 startingElement = 0; 294 } 295 } 296 297 // Build up an array of parts. These elements may be "" if there are 298 // extra slashes. 299 string[] longParts = new string[parts.Length - startingElement]; 300 301 string longPath = pathRoot; 302 for (int i = startingElement; i < parts.Length; ++i) 303 { 304 // If there is a zero-length part, then that means there was an extra slash. 305 if (parts[i].Length == 0) 306 { 307 longParts[i-startingElement] = String.Empty; 308 } 309 else 310 { 311 312 if (parts[i].IndexOf("~", StringComparison.Ordinal) == -1) 313 { 314 // If there's no ~, don't hit the disk. 315 longParts[i - startingElement] = parts[i]; 316 longPath = Path.Combine(longPath, parts[i]); 317 } 318 else 319 { 320 // getFileSystemEntries(...) returns an empty array if longPath doesn't exist. 321 string[] entries = getFileSystemEntries(FileSystemEntity.FilesAndDirectories, longPath, parts[i], null, false); 322 323 if (0 == entries.Length) 324 { 325 // The next part doesn't exist. Therefore, no more of the path will exist. 326 // Just return the rest. 327 for (int j = i; j<parts.Length; ++j) 328 { 329 longParts[j - startingElement] = parts[j]; 330 } 331 break; 332 } 333 334 // Since we know there are no wild cards, this should be length one. 335 ErrorUtilities.VerifyThrow(entries.Length == 1, 336 "Unexpected number of entries ({3}) found when enumerating '{0}' under '{1}'. Original path was '{2}'", 337 parts[i], longPath, path, entries.Length); 338 339 // Entries[0] contains the full path. 340 longPath = entries[0]; 341 342 // We just want the trailing node. 343 longParts[i - startingElement] = Path.GetFileName(longPath); 344 } 345 } 346 } 347 348 return pathRoot + String.Join (directorySeparator, longParts); 349 } 350 351 /// <summary> 352 /// Given a filespec, split it into left-most 'fixed' dir part, middle 'wildcard' dir part, and filename part. 353 /// The filename part may have wildcard characters in it. 354 /// </summary> 355 /// <param name="filespec">The filespec to be decomposed.</param> 356 /// <param name="fixedDirectoryPart">Receives the fixed directory part.</param> 357 /// <param name="wildcardDirectoryPart">The wildcard directory part.</param> 358 /// <param name="filenamePart">The filename part.</param> 359 /// <param name="getFileSystemEntries">Delegate.</param> SplitFileSpec( string filespec, out string fixedDirectoryPart, out string wildcardDirectoryPart, out string filenamePart, GetFileSystemEntries getFileSystemEntries )360 internal static void SplitFileSpec 361 ( 362 string filespec, 363 out string fixedDirectoryPart, 364 out string wildcardDirectoryPart, 365 out string filenamePart, 366 GetFileSystemEntries getFileSystemEntries 367 ) 368 { 369 PreprocessFileSpecForSplitting 370 ( 371 filespec, 372 out fixedDirectoryPart, 373 out wildcardDirectoryPart, 374 out filenamePart 375 ); 376 377 /* 378 * Handle the special case in which filenamePart is '**'. 379 * In this case, filenamePart becomes '*.*' and the '**' is appended 380 * to the end of the wildcardDirectory part. 381 * This is so that later regular expression matching can accurately 382 * pull out the different parts (fixed, wildcard, filename) of given 383 * file specs. 384 */ 385 if (recursiveDirectoryMatch == filenamePart) 386 { 387 wildcardDirectoryPart += recursiveDirectoryMatch; 388 wildcardDirectoryPart += directorySeparator; 389 filenamePart = "*.*"; 390 } 391 392 fixedDirectoryPart = FileMatcher.GetLongPathName(fixedDirectoryPart, getFileSystemEntries); 393 } 394 395 /// <summary> 396 /// Do most of the grunt work of splitting the filespec into parts. 397 /// Does not handle post-processing common to the different matching 398 /// paths. 399 /// </summary> 400 /// <param name="filespec">The filespec to be decomposed.</param> 401 /// <param name="fixedDirectoryPart">Receives the fixed directory part.</param> 402 /// <param name="wildcardDirectoryPart">The wildcard directory part.</param> 403 /// <param name="filenamePart">The filename part.</param> PreprocessFileSpecForSplitting( string filespec, out string fixedDirectoryPart, out string wildcardDirectoryPart, out string filenamePart )404 private static void PreprocessFileSpecForSplitting 405 ( 406 string filespec, 407 out string fixedDirectoryPart, 408 out string wildcardDirectoryPart, 409 out string filenamePart 410 ) 411 { 412 int indexOfLastDirectorySeparator = filespec.LastIndexOfAny(directorySeparatorCharacters); 413 if (-1 == indexOfLastDirectorySeparator) 414 { 415 /* 416 * No dir separator found. This is either this form, 417 * 418 * Source.cs 419 * *.cs 420 * 421 * or this form, 422 * 423 * ** 424 */ 425 fixedDirectoryPart = String.Empty; 426 wildcardDirectoryPart = String.Empty; 427 filenamePart = filespec; 428 return; 429 } 430 431 int indexOfFirstWildcard = filespec.IndexOfAny(wildcardCharacters); 432 if 433 ( 434 -1 == indexOfFirstWildcard 435 || indexOfFirstWildcard > indexOfLastDirectorySeparator 436 ) 437 { 438 /* 439 * There is at least one dir separator, but either there is no wild card or the 440 * wildcard is after the dir separator. 441 * 442 * The form is one of these: 443 * 444 * dir1\Source.cs 445 * dir1\*.cs 446 * 447 * Where the trailing spec is meant to be a filename. Or, 448 * 449 * dir1\** 450 * 451 * Where the trailing spec is meant to be any file recursively. 452 */ 453 454 // We know the fixed director part now. 455 fixedDirectoryPart = filespec.Substring (0, indexOfLastDirectorySeparator + 1); 456 wildcardDirectoryPart = String.Empty; 457 filenamePart = filespec.Substring (indexOfLastDirectorySeparator + 1); 458 return; 459 } 460 461 /* 462 * Find the separator right before the first wildcard. 463 */ 464 string filespecLeftOfWildcard = filespec.Substring(0, indexOfFirstWildcard); 465 int indexOfSeparatorBeforeWildCard = filespecLeftOfWildcard.LastIndexOfAny(directorySeparatorCharacters); 466 if (-1 == indexOfSeparatorBeforeWildCard) 467 { 468 /* 469 * There is no separator before the wildcard, so the form is like this: 470 * 471 * dir?\Source.cs 472 * 473 * or this, 474 * 475 * dir?\** 476 */ 477 fixedDirectoryPart = String.Empty; 478 wildcardDirectoryPart = filespec.Substring (0, indexOfLastDirectorySeparator + 1); 479 filenamePart = filespec.Substring (indexOfLastDirectorySeparator + 1); 480 return; 481 } 482 483 /* 484 * There is at least one wildcard and one dir separator, split parts out. 485 */ 486 fixedDirectoryPart = filespec.Substring(0, indexOfSeparatorBeforeWildCard+1); 487 wildcardDirectoryPart = filespec.Substring(indexOfSeparatorBeforeWildCard+1, indexOfLastDirectorySeparator-indexOfSeparatorBeforeWildCard); 488 filenamePart = filespec.Substring(indexOfLastDirectorySeparator+1); 489 } 490 491 /// <summary> 492 /// Removes the leading ".\" from all of the paths in the array. 493 /// </summary> 494 /// <param name="paths">Paths to remove .\ from.</param> RemoveInitialDotSlash( string[] paths )495 private static void RemoveInitialDotSlash 496 ( 497 string[] paths 498 ) 499 { 500 for (int i=0; i < paths.Length; i++) 501 { 502 if (paths[i].StartsWith(".\\", StringComparison.Ordinal)) 503 { 504 paths[i] = paths[i].Substring(2); 505 } 506 } 507 } 508 509 510 /// <summary> 511 /// Checks if the char is a DirectorySeparatorChar or a AltDirectorySeparatorChar 512 /// </summary> 513 /// <param name="c"></param> 514 /// <returns></returns> IsDirectorySeparator(char c)515 internal static bool IsDirectorySeparator(char c) 516 { 517 return (c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar); 518 } 519 /// <summary> 520 /// Removes the current directory converting the file back to relative path 521 /// </summary> 522 /// <param name="paths">Paths to remove current directory from.</param> 523 /// <param name="projectDirectory"></param> RemoveProjectDirectory( string[] paths, string projectDirectory )524 internal static void RemoveProjectDirectory 525 ( 526 string[] paths, 527 string projectDirectory 528 ) 529 { 530 bool directoryLastCharIsSeparator = IsDirectorySeparator(projectDirectory[projectDirectory.Length - 1]); 531 for (int i = 0; i < paths.Length; i++) 532 { 533 if (paths[i].StartsWith(projectDirectory, StringComparison.Ordinal)) 534 { 535 // If the project directory did not end in a slash we need to check to see if the next char in the path is a slash 536 if (!directoryLastCharIsSeparator) 537 { 538 //If the next char after the project directory is not a slash, skip this path 539 if (paths[i].Length <= projectDirectory.Length || !IsDirectorySeparator(paths[i][projectDirectory.Length])) 540 { 541 continue; 542 } 543 paths[i] = paths[i].Substring(projectDirectory.Length + 1); 544 } 545 else 546 { 547 paths[i] = paths[i].Substring(projectDirectory.Length); 548 } 549 } 550 } 551 } 552 553 /// <summary> 554 /// Get all files that match either the file-spec or the regular expression. 555 /// </summary> 556 /// <param name="listOfFiles">List of files that gets populated.</param> 557 /// <param name="baseDirectory">The path to enumerate</param> 558 /// <param name="remainingWildcardDirectory">The remaining, wildcard part of the directory.</param> 559 /// <param name="filespec">The filespec.</param> 560 /// <param name="extensionLengthToEnforce"></param> 561 /// <param name="regexFileMatch">Wild-card matching.</param> 562 /// <param name="needsRecursion">If true, then recursion is required.</param> 563 /// <param name="projectDirectory"></param> 564 /// <param name="stripProjectDirectory"></param> 565 /// <param name="getFileSystemEntries">Delegate.</param> GetFilesRecursive( System.Collections.IList listOfFiles, string baseDirectory, string remainingWildcardDirectory, string filespec, int extensionLengthToEnforce, Regex regexFileMatch, bool needsRecursion, string projectDirectory, bool stripProjectDirectory, GetFileSystemEntries getFileSystemEntries )566 private static void GetFilesRecursive 567 ( 568 System.Collections.IList listOfFiles, 569 string baseDirectory, 570 string remainingWildcardDirectory, 571 string filespec, // can be null 572 int extensionLengthToEnforce, // only relevant when filespec is not null 573 Regex regexFileMatch, // can be null 574 bool needsRecursion, 575 string projectDirectory, 576 bool stripProjectDirectory, 577 GetFileSystemEntries getFileSystemEntries 578 ) 579 { 580 Debug.Assert((filespec == null) || (regexFileMatch == null), 581 "File-spec overrides the regular expression -- pass null for file-spec if you want to use the regular expression."); 582 583 ErrorUtilities.VerifyThrow((filespec != null) || (regexFileMatch != null), 584 "Need either a file-spec or a regular expression to match files."); 585 586 ErrorUtilities.VerifyThrow(remainingWildcardDirectory!=null, "Expected non-null remaning wildcard directory."); 587 588 /* 589 * Get the matching files. 590 */ 591 bool considerFiles = false; 592 593 // Only consider files if... 594 if (remainingWildcardDirectory.Length == 0) 595 { 596 // We've reached the end of the wildcard directory elements. 597 considerFiles = true; 598 } 599 else if (remainingWildcardDirectory.IndexOf(recursiveDirectoryMatch, StringComparison.Ordinal) == 0) 600 { 601 // or, we've reached a "**" so everything else is matched recursively. 602 considerFiles = true; 603 } 604 605 if (considerFiles) 606 { 607 string[] files = getFileSystemEntries(FileSystemEntity.Files, baseDirectory, filespec, projectDirectory, stripProjectDirectory); 608 foreach (string file in files) 609 { 610 if ((filespec != null) || 611 // if no file-spec provided, match the file to the regular expression 612 // PERF NOTE: Regex.IsMatch() is an expensive operation, so we avoid it whenever possible 613 regexFileMatch.IsMatch(file)) 614 { 615 if ((filespec == null) || 616 // if we used a file-spec with a "loosely" defined extension 617 (extensionLengthToEnforce == 0) || 618 // discard all files that do not have extensions of the desired length 619 (Path.GetExtension(file).Length == extensionLengthToEnforce)) 620 { 621 listOfFiles.Add((object)file); 622 } 623 } 624 } 625 } 626 627 /* 628 * Recurse into subdirectories. 629 */ 630 if (needsRecursion && remainingWildcardDirectory.Length>0) 631 { 632 // Find the next directory piece. 633 string pattern = null; 634 635 if (remainingWildcardDirectory != recursiveDirectoryMatch) 636 { 637 int indexOfNextSlash = remainingWildcardDirectory.IndexOfAny(directorySeparatorCharacters); 638 ErrorUtilities.VerifyThrow(indexOfNextSlash != -1, "Slash should be guaranteed."); 639 640 // Peel off the leftmost directory piece. So for example, if remainingWildcardDirectory 641 // contains: 642 // 643 // ?emp\foo\**\bar 644 // 645 // then put '?emp' into pattern. Then put the remaining part, 646 // 647 // foo\**\bar 648 // 649 // back into remainingWildcardDirectory. 650 // This is a performance optimization. We don't want to enumerate everything if we 651 // don't have to. 652 pattern = remainingWildcardDirectory.Substring(0, indexOfNextSlash); 653 remainingWildcardDirectory = remainingWildcardDirectory.Substring(indexOfNextSlash + 1); 654 655 // If pattern turned into **, then there's no choice but to enumerate everything. 656 if (pattern == recursiveDirectoryMatch) 657 { 658 pattern = null; 659 remainingWildcardDirectory = recursiveDirectoryMatch; 660 } 661 } 662 663 // We never want to strip the project directory from the leaves, because the current 664 // process directory maybe different 665 string[] subdirs = getFileSystemEntries(FileSystemEntity.Directories, baseDirectory, pattern, null, false); 666 foreach (string subdir in subdirs) 667 { 668 GetFilesRecursive(listOfFiles, subdir, remainingWildcardDirectory, filespec, extensionLengthToEnforce, regexFileMatch, true, projectDirectory, stripProjectDirectory, getFileSystemEntries); 669 } 670 } 671 } 672 673 /// <summary> 674 /// Given a file spec, create a regular expression that will match that 675 /// file spec. 676 /// 677 /// PERF WARNING: this method is called in performance-critical 678 /// scenarios, so keep it fast and cheap 679 /// </summary> 680 /// <param name="fixedDirectoryPart">The fixed directory part.</param> 681 /// <param name="wildcardDirectoryPart">The wildcard directory part.</param> 682 /// <param name="filenamePart">The filename part.</param> 683 /// <param name="isLegalFileSpec">Receives whether this pattern is legal or not.</param> 684 /// <returns>The regular expression string.</returns> RegularExpressionFromFileSpec( string fixedDirectoryPart, string wildcardDirectoryPart, string filenamePart, out bool isLegalFileSpec )685 private static string RegularExpressionFromFileSpec 686 ( 687 string fixedDirectoryPart, 688 string wildcardDirectoryPart, 689 string filenamePart, 690 out bool isLegalFileSpec 691 ) 692 { 693 isLegalFileSpec = true; 694 695 /* 696 * The code below uses tags in the form <:tag:> to encode special information 697 * while building the regular expression. 698 * 699 * This format was chosen because it's not a legal form for filespecs. If the 700 * filespec comes in with either "<:" or ":>", return isLegalFileSpec=false to 701 * prevent intrusion into the special processing. 702 */ 703 if ((fixedDirectoryPart.IndexOf("<:", StringComparison.Ordinal) != -1) || 704 (fixedDirectoryPart.IndexOf(":>", StringComparison.Ordinal) != -1) || 705 (wildcardDirectoryPart.IndexOf("<:", StringComparison.Ordinal) != -1) || 706 (wildcardDirectoryPart.IndexOf(":>", StringComparison.Ordinal) != -1) || 707 (filenamePart.IndexOf("<:", StringComparison.Ordinal) != -1) || 708 (filenamePart.IndexOf(":>", StringComparison.Ordinal) != -1)) 709 { 710 isLegalFileSpec = false; 711 return String.Empty; 712 } 713 714 /* 715 * Its not legal for there to be a ".." after a wildcard. 716 */ 717 if (wildcardDirectoryPart.Contains(dotdot)) 718 { 719 isLegalFileSpec = false; 720 return String.Empty; 721 } 722 723 /* 724 * Trailing dots in file names have to be treated specially. 725 * We want: 726 * 727 * *. to match foo 728 * 729 * but 'foo' doesn't have a trailing '.' so we need to handle this while still being careful 730 * not to match 'foo.txt' 731 */ 732 if (filenamePart.EndsWith(".", StringComparison.Ordinal)) 733 { 734 filenamePart = filenamePart.Replace("*", "<:anythingbutdot:>"); 735 filenamePart = filenamePart.Replace("?", "<:anysinglecharacterbutdot:>"); 736 filenamePart = filenamePart.Substring(0, filenamePart.Length - 1); 737 } 738 739 /* 740 * Now, build up the starting filespec but put tags in to identify where the fixedDirectory, 741 * wildcardDirectory and filenamePart are. Also tag the beginning of the line and the end of 742 * the line, so that we can identify patterns by whether they're on one end or the other. 743 */ 744 StringBuilder matchFileExpression = new StringBuilder(); 745 matchFileExpression.Append("<:bol:>"); 746 matchFileExpression.Append("<:fixeddir:>").Append(fixedDirectoryPart).Append("<:endfixeddir:>"); 747 matchFileExpression.Append("<:wildcarddir:>").Append(wildcardDirectoryPart).Append("<:endwildcarddir:>"); 748 matchFileExpression.Append("<:filename:>").Append(filenamePart).Append("<:endfilename:>"); 749 matchFileExpression.Append("<:eol:>"); 750 751 /* 752 * Call out our special matching characters. 753 */ 754 matchFileExpression.Replace(directorySeparator, "<:dirseparator:>"); 755 matchFileExpression.Replace(altDirectorySeparator, "<:dirseparator:>"); 756 757 /* 758 * Capture the leading \\ in UNC paths, so that the doubled slash isn't 759 * reduced in a later step. 760 */ 761 matchFileExpression.Replace("<:fixeddir:><:dirseparator:><:dirseparator:>", "<:fixeddir:><:uncslashslash:>"); 762 763 /* 764 * Iteratively reduce four cases involving directory separators 765 * 766 * (1) <:dirseparator:>.<:dirseparator:> -> <:dirseparator:> 767 * This is an identity, so for example, these two are equivalent, 768 * 769 * dir1\.\dir2 == dir1\dir2 770 * 771 * (2) <:dirseparator:><:dirseparator:> -> <:dirseparator:> 772 * Double directory separators are treated as a single directory separator, 773 * so, for example, this is an identity: 774 * 775 * f:\dir1\\dir2 == f:\dir1\dir2 776 * 777 * The single exemption is for UNC path names, like this: 778 * 779 * \\server\share != \server\share 780 * 781 * This case is handled by the <:uncslashslash:> which was substituted in 782 * a prior step. 783 * 784 * (3) <:fixeddir:>.<:dirseparator:>.<:dirseparator:> -> <:fixeddir:>.<:dirseparator:> 785 * A ".\" at the beginning of a line is equivalent to nothing, so: 786 * 787 * .\.\dir1\file.txt == .\dir1\file.txt 788 * 789 * (4) <:dirseparator:>.<:eol:> -> <:eol:> 790 * A "\." at the end of a line is equivalent to nothing, so: 791 * 792 * dir1\dir2\. == dir1\dir2 * 793 */ 794 int sizeBefore; 795 do 796 { 797 sizeBefore = matchFileExpression.Length; 798 799 // NOTE: all these replacements will necessarily reduce the expression length i.e. length will either reduce or 800 // stay the same through this loop 801 matchFileExpression.Replace("<:dirseparator:>.<:dirseparator:>", "<:dirseparator:>"); 802 matchFileExpression.Replace("<:dirseparator:><:dirseparator:>", "<:dirseparator:>"); 803 matchFileExpression.Replace("<:fixeddir:>.<:dirseparator:>.<:dirseparator:>", "<:fixeddir:>.<:dirseparator:>"); 804 matchFileExpression.Replace("<:dirseparator:>.<:endfilename:>", "<:endfilename:>"); 805 matchFileExpression.Replace("<:filename:>.<:endfilename:>", "<:filename:><:endfilename:>"); 806 807 ErrorUtilities.VerifyThrow(matchFileExpression.Length <= sizeBefore, 808 "Expression reductions cannot increase the length of the expression."); 809 810 } while (matchFileExpression.Length < sizeBefore); 811 812 /* 813 * Collapse **\** into **. 814 */ 815 do 816 { 817 sizeBefore = matchFileExpression.Length; 818 matchFileExpression.Replace(recursiveDirectoryMatch + "<:dirseparator:>" + recursiveDirectoryMatch, recursiveDirectoryMatch); 819 820 ErrorUtilities.VerifyThrow(matchFileExpression.Length <= sizeBefore, 821 "Expression reductions cannot increase the length of the expression."); 822 823 } while (matchFileExpression.Length < sizeBefore); 824 825 /* 826 * Call out legal recursion operators: 827 * 828 * fixed-directory + **\ 829 * \**\ 830 * **\** 831 * 832 */ 833 do 834 { 835 sizeBefore = matchFileExpression.Length; 836 matchFileExpression.Replace("<:dirseparator:>" + recursiveDirectoryMatch + "<:dirseparator:>", "<:middledirs:>"); 837 matchFileExpression.Replace("<:wildcarddir:>" + recursiveDirectoryMatch + "<:dirseparator:>", "<:wildcarddir:><:leftdirs:>"); 838 839 ErrorUtilities.VerifyThrow(matchFileExpression.Length <= sizeBefore, 840 "Expression reductions cannot increase the length of the expression."); 841 842 } while (matchFileExpression.Length < sizeBefore); 843 844 845 /* 846 * By definition, "**" must appear alone between directory slashes. If there is any remaining "**" then this is not 847 * a valid filespec. 848 */ 849 // NOTE: this condition is evaluated left-to-right -- this is important because we want the length BEFORE stripping 850 // any "**"s remaining in the expression 851 if (matchFileExpression.Length > matchFileExpression.Replace(recursiveDirectoryMatch, null).Length) 852 { 853 isLegalFileSpec = false; 854 return String.Empty; 855 } 856 857 /* 858 * Remaining call-outs not involving "**" 859 */ 860 matchFileExpression.Replace("*.*", "<:anynonseparator:>"); 861 matchFileExpression.Replace("*", "<:anynonseparator:>"); 862 matchFileExpression.Replace("?", "<:singlecharacter:>"); 863 864 /* 865 * Escape all special characters defined for regular expresssions. 866 */ 867 matchFileExpression.Replace("\\", "\\\\"); // Must be first. 868 matchFileExpression.Replace("$", "\\$"); 869 matchFileExpression.Replace("(", "\\("); 870 matchFileExpression.Replace(")", "\\)"); 871 matchFileExpression.Replace("*", "\\*"); 872 matchFileExpression.Replace("+", "\\+"); 873 matchFileExpression.Replace(".", "\\."); 874 matchFileExpression.Replace("[", "\\["); 875 matchFileExpression.Replace("?", "\\?"); 876 matchFileExpression.Replace("^", "\\^"); 877 matchFileExpression.Replace("{", "\\{"); 878 matchFileExpression.Replace("|", "\\|"); 879 880 /* 881 * Now, replace call-outs with their regex equivalents. 882 */ 883 matchFileExpression.Replace("<:middledirs:>", "((/)|(\\\\)|(/.*/)|(/.*\\\\)|(\\\\.*\\\\)|(\\\\.*/))"); 884 matchFileExpression.Replace("<:leftdirs:>", "((.*/)|(.*\\\\)|())"); 885 matchFileExpression.Replace("<:rightdirs:>", ".*"); 886 matchFileExpression.Replace("<:anything:>", ".*"); 887 matchFileExpression.Replace("<:anythingbutdot:>", "[^\\.]*"); 888 matchFileExpression.Replace("<:anysinglecharacterbutdot:>", "[^\\.]."); 889 matchFileExpression.Replace("<:anynonseparator:>", "[^/\\\\]*"); 890 matchFileExpression.Replace("<:singlecharacter:>", "."); 891 matchFileExpression.Replace("<:dirseparator:>", "[/\\\\]+"); 892 matchFileExpression.Replace("<:uncslashslash:>", @"\\\\"); 893 matchFileExpression.Replace("<:bol:>", "^"); 894 matchFileExpression.Replace("<:eol:>", "$"); 895 matchFileExpression.Replace("<:fixeddir:>", "(?<FIXEDDIR>"); 896 matchFileExpression.Replace("<:endfixeddir:>", ")"); 897 matchFileExpression.Replace("<:wildcarddir:>", "(?<WILDCARDDIR>"); 898 matchFileExpression.Replace("<:endwildcarddir:>", ")"); 899 matchFileExpression.Replace("<:filename:>", "(?<FILENAME>"); 900 matchFileExpression.Replace("<:endfilename:>", ")"); 901 902 return matchFileExpression.ToString(); 903 } 904 905 906 907 /// <summary> 908 /// Given a filespec, get the information needed for file matching. 909 /// </summary> 910 /// <param name="filespec">The filespec.</param> 911 /// <param name="regexFileMatch">Receives the regular expression.</param> 912 /// <param name="needsRecursion">Receives the flag that is true if recursion is required.</param> 913 /// <param name="isLegalFileSpec">Receives the flag that is true if the filespec is legal.</param> 914 /// <param name="getFileSystemEntries">Delegate.</param> GetFileSpecInfo( string filespec, out Regex regexFileMatch, out bool needsRecursion, out bool isLegalFileSpec, GetFileSystemEntries getFileSystemEntries )915 internal static void GetFileSpecInfo 916 ( 917 string filespec, 918 out Regex regexFileMatch, 919 out bool needsRecursion, 920 out bool isLegalFileSpec, 921 GetFileSystemEntries getFileSystemEntries 922 923 ) 924 { 925 string fixedDirectoryPart; 926 string wildcardDirectoryPart; 927 string filenamePart; 928 string matchFileExpression; 929 930 GetFileSpecInfo(filespec, 931 out fixedDirectoryPart, out wildcardDirectoryPart, out filenamePart, 932 out matchFileExpression, out needsRecursion, out isLegalFileSpec, 933 getFileSystemEntries); 934 935 if (isLegalFileSpec) 936 { 937 regexFileMatch = new Regex(matchFileExpression, RegexOptions.IgnoreCase); 938 } 939 else 940 { 941 regexFileMatch = null; 942 } 943 } 944 945 /// <summary> 946 /// Given a filespec, get the information needed for file matching. 947 /// </summary> 948 /// <param name="filespec">The filespec.</param> 949 /// <param name="fixedDirectoryPart">Receives the fixed directory part.</param> 950 /// <param name="wildcardDirectoryPart">Receives the wildcard directory part.</param> 951 /// <param name="filenamePart">Receives the filename part.</param> 952 /// <param name="matchFileExpression">Receives the regular expression.</param> 953 /// <param name="needsRecursion">Receives the flag that is true if recursion is required.</param> 954 /// <param name="isLegalFileSpec">Receives the flag that is true if the filespec is legal.</param> 955 /// <param name="getFileSystemEntries">Delegate.</param> GetFileSpecInfo( string filespec, out string fixedDirectoryPart, out string wildcardDirectoryPart, out string filenamePart, out string matchFileExpression, out bool needsRecursion, out bool isLegalFileSpec, GetFileSystemEntries getFileSystemEntries )956 private static void GetFileSpecInfo 957 ( 958 string filespec, 959 out string fixedDirectoryPart, 960 out string wildcardDirectoryPart, 961 out string filenamePart, 962 out string matchFileExpression, 963 out bool needsRecursion, 964 out bool isLegalFileSpec, 965 GetFileSystemEntries getFileSystemEntries 966 ) 967 { 968 isLegalFileSpec = true; 969 needsRecursion = false; 970 fixedDirectoryPart = String.Empty; 971 wildcardDirectoryPart = String.Empty; 972 filenamePart = String.Empty; 973 matchFileExpression = null; 974 975 // bail out if filespec contains illegal characters 976 if (-1 != filespec.IndexOfAny(Path.GetInvalidPathChars())) 977 { 978 isLegalFileSpec = false; 979 return; 980 } 981 982 /* 983 * Check for patterns in the filespec that are explicitly illegal. 984 * 985 * Any path with "..." in it is illegal. 986 */ 987 if (-1 != filespec.IndexOf("...", StringComparison.Ordinal)) 988 { 989 isLegalFileSpec = false; 990 return; 991 } 992 993 /* 994 * If there is a ':' anywhere but the second character, this is an illegal pattern. 995 * Catches this case among others, 996 * 997 * http://www.website.com 998 * 999 */ 1000 int rightmostColon = filespec.LastIndexOf(":", StringComparison.Ordinal); 1001 1002 if 1003 ( 1004 -1 != rightmostColon 1005 && 1 != rightmostColon 1006 ) 1007 { 1008 isLegalFileSpec = false; 1009 return; 1010 } 1011 1012 /* 1013 * Now break up the filespec into constituent parts--fixed, wildcard and filename. 1014 */ 1015 SplitFileSpec(filespec, out fixedDirectoryPart, out wildcardDirectoryPart, out filenamePart, getFileSystemEntries); 1016 1017 /* 1018 * Get a regular expression for matching files that will be found. 1019 */ 1020 matchFileExpression = RegularExpressionFromFileSpec(fixedDirectoryPart, wildcardDirectoryPart, filenamePart, out isLegalFileSpec); 1021 1022 /* 1023 * Was the filespec valid? If not, then just return now. 1024 */ 1025 if (!isLegalFileSpec) 1026 { 1027 return; 1028 } 1029 1030 /* 1031 * Determine whether recursion will be required. 1032 */ 1033 needsRecursion = (wildcardDirectoryPart.Length != 0); 1034 } 1035 1036 /// <summary> 1037 /// The results of a match between a filespec and a file name. 1038 /// </summary> 1039 internal sealed class Result 1040 { 1041 /// <summary> 1042 /// Default constructor. 1043 /// </summary> Result()1044 internal Result() 1045 { 1046 // do nothing 1047 } 1048 1049 internal bool isLegalFileSpec = false; 1050 internal bool isMatch = false; 1051 internal bool isFileSpecRecursive = false; 1052 internal string fixedDirectoryPart = String.Empty; 1053 internal string wildcardDirectoryPart = String.Empty; 1054 internal string filenamePart = String.Empty; 1055 } 1056 1057 /// <summary> 1058 /// Given a pattern (filespec) and a candidate filename (fileToMatch) 1059 /// return matching information. 1060 /// </summary> 1061 /// <param name="filespec">The filespec.</param> 1062 /// <param name="fileToMatch">The candidate to match against.</param> 1063 /// <returns>The result class.</returns> FileMatch( string filespec, string fileToMatch )1064 internal static Result FileMatch 1065 ( 1066 string filespec, 1067 string fileToMatch 1068 ) 1069 { 1070 return FileMatch(filespec, fileToMatch, defaultGetFileSystemEntries); 1071 } 1072 1073 /// <summary> 1074 /// Given a pattern (filespec) and a candidate filename (fileToMatch) 1075 /// return matching information. 1076 /// </summary> 1077 /// <param name="filespec">The filespec.</param> 1078 /// <param name="fileToMatch">The candidate to match against.</param> 1079 /// <param name="getFileSystemEntries">Delegate.</param> 1080 /// <returns>The result class.</returns> FileMatch( string filespec, string fileToMatch, GetFileSystemEntries getFileSystemEntries )1081 internal static Result FileMatch 1082 ( 1083 string filespec, 1084 string fileToMatch, 1085 GetFileSystemEntries getFileSystemEntries 1086 ) 1087 { 1088 Result matchResult = new Result(); 1089 1090 fileToMatch = GetLongPathName(fileToMatch, getFileSystemEntries); 1091 1092 Regex regexFileMatch; 1093 GetFileSpecInfo 1094 ( 1095 filespec, 1096 out regexFileMatch, 1097 out matchResult.isFileSpecRecursive, 1098 out matchResult.isLegalFileSpec, 1099 getFileSystemEntries 1100 ); 1101 1102 if (matchResult.isLegalFileSpec) 1103 { 1104 Match match = regexFileMatch.Match(fileToMatch); 1105 matchResult.isMatch = match.Success; 1106 1107 if (matchResult.isMatch) 1108 { 1109 matchResult.fixedDirectoryPart = match.Groups["FIXEDDIR"].Value; 1110 matchResult.wildcardDirectoryPart = match.Groups["WILDCARDDIR"].Value; 1111 matchResult.filenamePart = match.Groups["FILENAME"].Value; 1112 } 1113 } 1114 1115 return matchResult; 1116 } 1117 1118 /// <summary> 1119 /// Given a filespec, find the files that match. 1120 /// </summary> 1121 /// <param name="filespec">Get files that match the given file spec.</param> 1122 /// <returns>The array of files.</returns> GetFiles( string projectDirectory, string filespec )1123 internal static string[] GetFiles 1124 ( 1125 string projectDirectory, 1126 string filespec 1127 ) 1128 { 1129 string[] files = GetFiles(projectDirectory, filespec, defaultGetFileSystemEntries, defaultDirectoryExists); 1130 return files; 1131 } 1132 1133 /// <summary> 1134 /// Given a filespec, find the files that match. 1135 /// </summary> 1136 /// <param name="filespec">Get files that match the given file spec.</param> 1137 /// <param name="getFileSystemEntries">Get files that match the given file spec.</param> 1138 /// <param name="directoryExists">Determine whether a directory exists.</param> 1139 /// <returns>The array of files.</returns> GetFiles( string projectDirectory, string filespec, GetFileSystemEntries getFileSystemEntries, DirectoryExists directoryExists )1140 internal static string[] GetFiles 1141 ( 1142 string projectDirectory, 1143 string filespec, 1144 GetFileSystemEntries getFileSystemEntries, 1145 DirectoryExists directoryExists 1146 ) 1147 { 1148 // For performance. Short-circuit iff there is no wildcard. 1149 // Perf Note: Doing a [Last]IndexOfAny(...) is much faster than compiling a 1150 // regular expression that does the same thing, regardless of whether 1151 // filespec contains one of the characters. 1152 // Choose LastIndexOfAny instead of IndexOfAny because it seems more likely 1153 // that wildcards will tend to be towards the right side. 1154 if (!HasWildcards(filespec)) 1155 { 1156 return new string[] { filespec }; 1157 } 1158 1159 /* 1160 * Even though we return a string[] we work internally with an IList. 1161 * This is because it's cheaper to add items to an IList and this code 1162 * might potentially do a lot of that. 1163 */ 1164 System.Collections.ArrayList arrayListOfFiles = new System.Collections.ArrayList(); 1165 System.Collections.IList listOfFiles = (System.Collections.IList) arrayListOfFiles; 1166 1167 /* 1168 * Analyze the file spec and get the information we need to do the matching. 1169 */ 1170 string fixedDirectoryPart; 1171 string wildcardDirectoryPart; 1172 string filenamePart; 1173 string matchFileExpression; 1174 bool needsRecursion; 1175 bool isLegalFileSpec; 1176 GetFileSpecInfo 1177 ( 1178 filespec, 1179 out fixedDirectoryPart, 1180 out wildcardDirectoryPart, 1181 out filenamePart, 1182 out matchFileExpression, 1183 out needsRecursion, 1184 out isLegalFileSpec, 1185 getFileSystemEntries 1186 ); 1187 1188 /* 1189 * If the filespec is invalid, then just return now. 1190 */ 1191 if (!isLegalFileSpec) 1192 { 1193 return new string[] { filespec }; 1194 } 1195 1196 // The projectDirectory is not null only if we are running the evaluation from 1197 // inside the engine (i.e. not from a task) 1198 bool stripProjectDirectory = false; 1199 if (projectDirectory != null) 1200 { 1201 if (fixedDirectoryPart != null) 1202 { 1203 string oldFixedDirectoryPart = fixedDirectoryPart; 1204 try 1205 { 1206 fixedDirectoryPart = Path.Combine(projectDirectory, fixedDirectoryPart); 1207 } 1208 catch (ArgumentException) 1209 { 1210 return new string[0]; 1211 } 1212 1213 stripProjectDirectory = !String.Equals(fixedDirectoryPart, oldFixedDirectoryPart, StringComparison.OrdinalIgnoreCase); 1214 } 1215 else 1216 { 1217 fixedDirectoryPart = projectDirectory; 1218 stripProjectDirectory = true; 1219 } 1220 } 1221 1222 /* 1223 * If the fixed directory part doesn't exist, then this means no files should be 1224 * returned. 1225 */ 1226 if (fixedDirectoryPart.Length > 0 && !directoryExists(fixedDirectoryPart)) 1227 { 1228 return new string[0]; 1229 } 1230 1231 // determine if we need to use the regular expression to match the files 1232 // PERF NOTE: Constructing a Regex object is expensive, so we avoid it whenever possible 1233 bool matchWithRegex = 1234 // if we have a directory specification that uses wildcards, and 1235 (wildcardDirectoryPart.Length > 0) && 1236 // the specification is not a simple "**" 1237 (wildcardDirectoryPart != (recursiveDirectoryMatch + directorySeparator)); 1238 // then we need to use the regular expression 1239 1240 // if we're not using the regular expression, get the file pattern extension 1241 string extensionPart = matchWithRegex 1242 ? null 1243 : Path.GetExtension(filenamePart); 1244 1245 // check if the file pattern would cause Windows to match more loosely on the extension 1246 // NOTE: Windows matches loosely in two cases (in the absence of the * wildcard in the extension): 1247 // 1) if the extension ends with the ? wildcard, it matches files with shorter extensions also e.g. "file.tx?" would 1248 // match both "file.txt" and "file.tx" 1249 // 2) if the extension is three characters, and the filename contains the * wildcard, it matches files with longer 1250 // extensions that start with the same three characters e.g. "*.htm" would match both "file.htm" and "file.html" 1251 bool needToEnforceExtensionLength = 1252 (extensionPart != null) && 1253 (extensionPart.IndexOf('*') == -1) 1254 && 1255 (extensionPart.EndsWith("?", StringComparison.Ordinal) 1256 || 1257 ((extensionPart.Length == (3 + 1 /* +1 for the period */)) && 1258 (filenamePart.IndexOf('*') != -1))); 1259 1260 /* 1261 * Now get the files that match, starting at the lowest fixed directory. 1262 */ 1263 GetFilesRecursive(listOfFiles, fixedDirectoryPart, wildcardDirectoryPart, 1264 // if using the regular expression, ignore the file pattern 1265 (matchWithRegex ? null : filenamePart), (needToEnforceExtensionLength ? extensionPart.Length : 0), 1266 // if using the file pattern, ignore the regular expression 1267 (matchWithRegex ? new Regex(matchFileExpression, RegexOptions.IgnoreCase) : null), 1268 needsRecursion, projectDirectory, stripProjectDirectory, getFileSystemEntries); 1269 1270 /* 1271 * Build the return array. 1272 */ 1273 string[] files = (string[])arrayListOfFiles.ToArray(typeof(string)); 1274 return files; 1275 } 1276 } 1277 } 1278