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 #if !CLR2COMPATIBILITY
6 using System.Collections.Concurrent;
7 #endif
8 using System.Collections.Generic;
9 using System.Diagnostics;
10 using System.Diagnostics.CodeAnalysis;
11 using System.IO;
12 using System.Globalization;
13 using System.Linq;
14 using System.Runtime.InteropServices;
15 using System.Reflection;
16 using System.Runtime.CompilerServices;
17 using System.Text.RegularExpressions;
18 using System.Text;
19 using System.Threading;
20 using Microsoft.Build.Utilities;
21 
22 namespace Microsoft.Build.Shared
23 {
24     /// <summary>
25     /// This class contains utility methods for file IO.
26     /// PERF\COVERAGE NOTE: Try to keep classes in 'shared' as granular as possible. All the methods in
27     /// each class get pulled into the resulting assembly.
28     /// </summary>
29     internal static partial class FileUtilities
30     {
31         // A list of possible test runners. If the program running has one of these substrings in the name, we assume
32         // this is a test harness.
33 
34 
35         // This flag, when set, indicates that we are running tests. Initially assume it's true. It also implies that
36         // the currentExecutableOverride is set to a path (that is non-null). Assume this is not initialized when we
37         // have the impossible combination of runningTests = false and currentExecutableOverride = null.
38 
39         // This is the fake current executable we use in case we are running tests.
40 
41         // MaxPath accounts for the null-terminating character, for example, the maximum path on the D drive is "D:\<256 chars>\0".
42         // See: ndp\clr\src\BCL\System\IO\Path.cs
43         internal const int MaxPath = 260;
44 
45         /// <summary>
46         /// The directory where MSBuild stores cache information used during the build.
47         /// </summary>
48         internal static string cacheDirectory = null;
49 
50         /// <summary>
51         /// FOR UNIT TESTS ONLY
52         /// Clear out the static variable used for the cache directory so that tests that
53         /// modify it can validate their modifications.
54         /// </summary>
ClearCacheDirectoryPath()55         internal static void ClearCacheDirectoryPath()
56         {
57             cacheDirectory = null;
58         }
59 
60         // TODO: assumption on file system case sensitivity: https://github.com/Microsoft/msbuild/issues/781
61         internal static readonly StringComparison PathComparison = StringComparison.OrdinalIgnoreCase;
62 
63         /// <summary>
64         /// Copied from https://github.com/dotnet/corefx/blob/056715ff70e14712419d82d51c8c50c54b9ea795/src/Common/src/System/IO/PathInternal.Windows.cs#L61
65         /// MSBuild should support the union of invalid path chars across the supported OSes, so builds can have the same behaviour crossplatform: https://github.com/Microsoft/msbuild/issues/781#issuecomment-243942514
66         /// </summary>
67 
68         internal static readonly char[] InvalidPathChars = new char[]
69         {
70             '|', '\0',
71             (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
72             (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
73             (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
74             (char)31
75         };
76 
77         /// <summary>
78         /// Copied from https://github.com/dotnet/corefx/blob/387cf98c410bdca8fd195b28cbe53af578698f94/src/System.Runtime.Extensions/src/System/IO/Path.Windows.cs#L18
79         /// MSBuild should support the union of invalid path chars across the supported OSes, so builds can have the same behaviour crossplatform: https://github.com/Microsoft/msbuild/issues/781#issuecomment-243942514
80         /// </summary>
81         internal static readonly char[] InvalidFileNameChars = new char[]
82         {
83             '\"', '<', '>', '|', '\0',
84             (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
85             (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
86             (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
87             (char)31, ':', '*', '?', '\\', '/'
88         };
89 
90         private static readonly char[] Slashes = { '/', '\\' };
91 
92 #if !CLR2COMPATIBILITY
93         private static ConcurrentDictionary<string, bool> FileExistenceCache = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
94 #endif
95 
96         /// <summary>
97         /// Retrieves the MSBuild runtime cache directory
98         /// </summary>
GetCacheDirectory()99         internal static string GetCacheDirectory()
100         {
101             if (cacheDirectory == null)
102             {
103                 cacheDirectory = Path.Combine(Path.GetTempPath(), String.Format(CultureInfo.CurrentUICulture, "MSBuild{0}", Process.GetCurrentProcess().Id));
104             }
105 
106             return cacheDirectory;
107         }
108 
109         /// <summary>
110         /// Get the hex hash string for the string
111         /// </summary>
GetHexHash(string stringToHash)112         internal static string GetHexHash(string stringToHash)
113         {
114             return stringToHash.GetHashCode().ToString("X", CultureInfo.InvariantCulture);
115         }
116 
117         /// <summary>
118         /// Get the hash for the assemblyPaths
119         /// </summary>
GetPathsHash(IEnumerable<string> assemblyPaths)120         internal static int GetPathsHash(IEnumerable<string> assemblyPaths)
121         {
122             StringBuilder builder = new StringBuilder();
123 
124             foreach (string path in assemblyPaths)
125             {
126                 if (path != null)
127                 {
128                     string directoryPath = path.Trim();
129                     if (directoryPath.Length > 0)
130                     {
131                         DateTime lastModifiedTime;
132                         if (NativeMethodsShared.GetLastWriteDirectoryUtcTime(directoryPath, out lastModifiedTime))
133                         {
134                             builder.Append(lastModifiedTime.Ticks);
135                             builder.Append('|');
136                             builder.Append(directoryPath.ToUpperInvariant());
137                             builder.Append('|');
138                         }
139                     }
140                 }
141             }
142 
143             return builder.ToString().GetHashCode();
144         }
145 
146         /// <summary>
147         /// Clears the MSBuild runtime cache
148         /// </summary>
ClearCacheDirectory()149         internal static void ClearCacheDirectory()
150         {
151             string cacheDirectory = GetCacheDirectory();
152 
153             if (Directory.Exists(cacheDirectory))
154             {
155                 DeleteDirectoryNoThrow(cacheDirectory, true);
156             }
157         }
158 
159         /// <summary>
160         /// If the given path doesn't have a trailing slash then add one.
161         /// If the path is an empty string, does not modify it.
162         /// </summary>
163         /// <param name="fileSpec">The path to check.</param>
164         /// <returns>A path with a slash.</returns>
EnsureTrailingSlash(string fileSpec)165         internal static string EnsureTrailingSlash(string fileSpec)
166         {
167             fileSpec = FixFilePath(fileSpec);
168             if (fileSpec.Length > 0 && !EndsWithSlash(fileSpec))
169             {
170                 fileSpec += Path.DirectorySeparatorChar;
171             }
172 
173             return fileSpec;
174         }
175 
176         /// <summary>
177         /// Ensures the path does not have a leading slash.
178         /// </summary>
EnsureNoLeadingSlash(string path)179         internal static string EnsureNoLeadingSlash(string path)
180         {
181             path = FixFilePath(path);
182             if (path.Length > 0 && IsSlash(path[0]))
183             {
184                 path = path.Substring(1);
185             }
186 
187             return path;
188         }
189 
190         /// <summary>
191         /// Ensures the path does not have a trailing slash.
192         /// </summary>
EnsureNoTrailingSlash(string path)193         internal static string EnsureNoTrailingSlash(string path)
194         {
195             path = FixFilePath(path);
196             if (EndsWithSlash(path))
197             {
198                 path = path.Substring(0, path.Length - 1);
199             }
200 
201             return path;
202         }
203 
204         /// <summary>
205         /// Indicates if the given file-spec ends with a slash.
206         /// </summary>
207         /// <param name="fileSpec">The file spec.</param>
208         /// <returns>true, if file-spec has trailing slash</returns>
EndsWithSlash(string fileSpec)209         internal static bool EndsWithSlash(string fileSpec)
210         {
211             return (fileSpec.Length > 0)
212                 ? IsSlash(fileSpec[fileSpec.Length - 1])
213                 : false;
214         }
215 
216         /// <summary>
217         /// Indicates if the given character is a slash.
218         /// </summary>
219         /// <param name="c"></param>
220         /// <returns>true, if slash</returns>
IsSlash(char c)221         internal static bool IsSlash(char c)
222         {
223             return ((c == Path.DirectorySeparatorChar) || (c == Path.AltDirectorySeparatorChar));
224         }
225 
226         /// <summary>
227         /// Trims the string and removes any double quotes around it.
228         /// </summary>
TrimAndStripAnyQuotes(string path)229         internal static string TrimAndStripAnyQuotes(string path)
230         {
231             // Trim returns the same string if trimming isn't needed
232             path = path.Trim();
233             path = path.Trim(new char[] { '"' });
234 
235             return path;
236         }
237 
238         /// <summary>
239         /// Get the directory name of a rooted full path
240         /// </summary>
241         /// <param name="fullPath"></param>
242         /// <returns></returns>
GetDirectoryNameOfFullPath(String fullPath)243         internal static String GetDirectoryNameOfFullPath(String fullPath)
244         {
245             if (fullPath != null)
246             {
247                 int i = fullPath.Length;
248                 while (i > 0 && fullPath[--i] != Path.DirectorySeparatorChar && fullPath[i] != Path.AltDirectorySeparatorChar) ;
249                 return FixFilePath(fullPath.Substring(0, i));
250             }
251             return null;
252         }
253 
254         /// <summary>
255         /// Compare an unsafe char buffer with a <see cref="System.String"/> to see if their contents are identical.
256         /// </summary>
257         /// <param name="buffer">The beginning of the char buffer.</param>
258         /// <param name="len">The length of the buffer.</param>
259         /// <param name="s">The string.</param>
260         /// <returns>True only if the contents of <paramref name="s"/> and the first <paramref name="len"/> characters in <paramref name="buffer"/> are identical.</returns>
AreStringsEqual(char* buffer, int len, string s)261         private unsafe static bool AreStringsEqual(char* buffer, int len, string s)
262         {
263             if (len != s.Length)
264             {
265                 return false;
266             }
267 
268             foreach (char ch in s)
269             {
270                 if (ch != *buffer++)
271                 {
272                     return false;
273                 }
274             }
275 
276             return true;
277         }
278 
279         /// <summary>
280         /// Gets the canonicalized full path of the provided path.
281         /// Path.GetFullPath The pre .Net 4.6.2 implementation of Path.GetFullPath is slow and creates strings in its work.
282         /// Therefore MSBuild has its own implementation on full framework.
283         /// Guidance for use: call this on all paths accepted through public entry
284         /// points that need normalization. After that point, only verify the path
285         /// is rooted, using ErrorUtilities.VerifyThrowPathRooted.
286         /// ASSUMES INPUT IS ALREADY UNESCAPED.
287         /// </summary>
NormalizePath(string path)288         internal static string NormalizePath(string path)
289         {
290             ErrorUtilities.VerifyThrowArgumentLength(path, "path");
291 
292 #if FEATURE_LEGACY_GETFULLPATH
293 
294             if (NativeMethodsShared.IsWindows)
295             {
296                 int errorCode = 0; // 0 == success in Win32
297 
298 #if _DEBUG
299                 // Just to make sure and exercise the code that sets the correct buffer size
300                 // we'll start out with it deliberately too small
301                 int lenDir = 1;
302 #else
303                 int lenDir = MaxPath;
304 #endif
305                 unsafe
306                 {
307                     char* finalBuffer = stackalloc char[lenDir + 1]; // One extra for the null terminator
308 
309                     int length = NativeMethodsShared.GetFullPathName(path, lenDir + 1, finalBuffer, IntPtr.Zero);
310                     errorCode = Marshal.GetLastWin32Error();
311 
312                     // If the length returned from GetFullPathName is greater than the length of the buffer we've
313                     // allocated, then reallocate the buffer with the correct size, and repeat the call
314                     if (length > lenDir)
315                     {
316                         lenDir = length;
317                         char* tempBuffer = stackalloc char[lenDir];
318                         finalBuffer = tempBuffer;
319                         length = NativeMethodsShared.GetFullPathName(path, lenDir, finalBuffer, IntPtr.Zero);
320                         errorCode = Marshal.GetLastWin32Error();
321                         // If we find that the length returned from GetFullPathName is longer than the buffer capacity, then
322                         // something very strange is going on!
323                         ErrorUtilities.VerifyThrow(
324                             length <= lenDir,
325                             "Final buffer capacity should be sufficient for full path name and null terminator.");
326                     }
327 
328                     if (length > 0)
329                     {
330                         // In order to prevent people from taking advantage of our ability to extend beyond MaxPath
331                         // since it is unlikely that the CLR fix will be a complete removal of maxpath madness
332                         // we reluctantly have to restrict things here.
333                         if (length >= MaxPath)
334                         {
335                             throw new PathTooLongException();
336                         }
337 
338                         // Avoid creating new strings unnecessarily
339                         string finalFullPath = AreStringsEqual(finalBuffer, length, path)
340                             ? path
341                             : new string(
342                                 finalBuffer,
343                                 startIndex: 0,
344                                 length: length);
345 
346                         // We really don't care about extensions here, but Path.HasExtension provides a great way to
347                         // invoke the CLR's invalid path checks (these are independent of path length)
348                         Path.HasExtension(finalFullPath);
349 
350                         if (finalFullPath.StartsWith(@"\\", StringComparison.Ordinal))
351                         {
352                             // If we detect we are a UNC path then we need to use the regular get full path in order to do the correct checks for UNC formatting
353                             // and security checks for strings like \\?\GlobalRoot
354                             int startIndex = 2;
355                             while (startIndex < finalFullPath.Length)
356                             {
357                                 if (finalFullPath[startIndex] == '\\')
358                                 {
359                                     startIndex++;
360                                     break;
361                                 }
362                                 else
363                                 {
364                                     startIndex++;
365                                 }
366                             }
367 
368                             /*
369                               From Path.cs in the CLR
370 
371                               Throw an ArgumentException for paths like \\, \\server, \\server\
372                               This check can only be properly done after normalizing, so
373                               \\foo\.. will be properly rejected.  Also, reject \\?\GLOBALROOT\
374                               (an internal kernel path) because it provides aliases for drives.
375 
376                               throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegalUNC"));
377 
378                                // Check for \\?\Globalroot, an internal mechanism to the kernel
379                                // that provides aliases for drives and other undocumented stuff.
380                                // The kernel team won't even describe the full set of what
381                                // is available here - we don't want managed apps mucking
382                                // with this for security reasons.
383                             */
384                     if (startIndex == finalFullPath.Length || finalFullPath.IndexOf(@"\\?\globalroot", PathComparison) != -1)
385                     {
386                         finalFullPath = Path.GetFullPath(finalFullPath);
387                     }
388                 }
389 
390                         return finalFullPath;
391                     }
392                 }
393 
394                 NativeMethodsShared.ThrowExceptionForErrorCode(errorCode);
395                 return null;
396             }
397 #endif
398             return FixFilePath(Path.GetFullPath(path));
399 
400         }
401 
NormalizePath(string directory, string file)402         internal static string NormalizePath(string directory, string file)
403         {
404             return NormalizePath(Path.Combine(directory, file));
405         }
406 
407 #if !CLR2COMPATIBILITY
NormalizePath(params string[] paths)408         internal static string NormalizePath(params string[] paths)
409         {
410             return NormalizePath(Path.Combine(paths));
411         }
412 #endif
413 
FixFilePath(string path)414         internal static string FixFilePath(string path)
415         {
416             return string.IsNullOrEmpty(path) || Path.DirectorySeparatorChar == '\\' ? path : path.Replace('\\', '/');//.Replace("//", "/");
417         }
418 
419         /// <summary>
420         /// If on Unix, convert backslashes to slashes for strings that resemble paths.
421         /// The heuristic is if something resembles paths (contains slashes) check if the
422         /// first segment exists and is a directory.
423         /// Use a native shared method to massage file path. If the file is adjusted,
424         /// that qualifies is as a path.
425         ///
426         /// @baseDirectory is just passed to LooksLikeUnixFilePath, to help with the check
427         /// </summary>
MaybeAdjustFilePath(string value, string baseDirectory=R)428         internal static string MaybeAdjustFilePath(string value, string baseDirectory="")
429         {
430             // Don't bother with arrays or properties or network paths, or those that
431             // have no slashes.
432             if (NativeMethodsShared.IsWindows || string.IsNullOrEmpty(value) ||
433                 value.StartsWith("$(") || value.StartsWith("@(") || value.StartsWith("\\\\") ||
434                 value.IndexOfAny(Slashes) == -1)
435             {
436                 return value;
437             }
438 
439             // For Unix-like systems, we may want to convert backslashes to slashes
440             string newValue = Regex.Replace(value, @"[\\/]+", "/");
441 
442             string quote = string.Empty;
443             // Find the part of the name we want to check, that is remove quotes, if present
444             string checkValue = newValue;
445             if (newValue.Length > 2)
446             {
447                 if (newValue.StartsWith("'"))
448                 {
449                     if (newValue.EndsWith("'"))
450                     {
451                         checkValue = newValue.Substring(1, newValue.Length - 2);
452                         quote = "'";
453                     }
454                 }
455                 else if (newValue.StartsWith("\"") && newValue.EndsWith("\""))
456                 {
457                     checkValue = newValue.Substring(1, newValue.Length - 2);
458                     quote = "\"";
459                 }
460             }
461 
462             return LooksLikeUnixFilePath(checkValue, baseDirectory) ? newValue : value;
463         }
464 
465         /// <summary>
466         /// If on Unix, check if the string looks like a file path.
467         /// The heuristic is if something resembles paths (contains slashes) check if the
468         /// first segment exists and is a directory.
469         ///
470         /// If @baseDirectory is not null, then look for the first segment exists under
471         /// that
472         /// </summary>
LooksLikeUnixFilePath(string value, string baseDirectory=R)473         internal static bool LooksLikeUnixFilePath(string value, string baseDirectory="")
474         {
475             if (!NativeMethodsShared.IsUnixLike)
476             {
477                 return false;
478             }
479 
480             var firstSlash = value.IndexOf('/');
481 
482             // The first slash will either be at the beginning of the string or after the first directory name
483             if (firstSlash == 0)
484             {
485                 firstSlash = value.Substring(1).IndexOf('/') + 1;
486             }
487 
488             if (firstSlash > 0 && Directory.Exists(Path.Combine(baseDirectory, value.Substring(0, firstSlash))))
489             {
490                 return true;
491             }
492 
493             // Check for actual files or directories under / that get missed by the above logic
494             if (firstSlash == 0 && value[0] == '/' && (Directory.Exists(value) || File.Exists(value)))
495             {
496                 return true;
497             }
498 
499             return false;
500         }
501 
502         /// <summary>
503         /// Extracts the directory from the given file-spec.
504         /// </summary>
505         /// <param name="fileSpec">The filespec.</param>
506         /// <returns>directory path</returns>
GetDirectory(string fileSpec)507         internal static string GetDirectory(string fileSpec)
508         {
509             string directory = Path.GetDirectoryName(FixFilePath(fileSpec));
510 
511             // if file-spec is a root directory e.g. c:, c:\, \, \\server\share
512             // NOTE: Path.GetDirectoryName also treats invalid UNC file-specs as root directories e.g. \\, \\server
513             if (directory == null)
514             {
515                 // just use the file-spec as-is
516                 directory = fileSpec;
517             }
518             else if ((directory.Length > 0) && !EndsWithSlash(directory))
519             {
520                 // restore trailing slash if Path.GetDirectoryName has removed it (this happens with non-root directories)
521                 directory += Path.DirectorySeparatorChar;
522             }
523 
524             return directory;
525         }
526 
527         /// <summary>
528         /// Determines whether the given assembly file name has one of the listed extensions.
529         /// </summary>
530         /// <param name="fileName">The name of the file</param>
531         /// <param name="allowedExtensions">Array of extensions to consider.</param>
532         /// <returns></returns>
HasExtension(string fileName, string[] allowedExtensions)533         internal static bool HasExtension(string fileName, string[] allowedExtensions)
534         {
535             Debug.Assert(allowedExtensions != null && allowedExtensions.Length > 0);
536 
537             // Easiest way to invoke invalid path chars
538             // check, which callers are relying on.
539             if (Path.HasExtension(fileName))
540             {
541                 foreach (string extension in allowedExtensions)
542                 {
543                     Debug.Assert(!String.IsNullOrEmpty(extension) && extension[0] == '.');
544 
545                     if (fileName.EndsWith(extension, PathComparison))
546                     {
547                         return true;
548                     }
549                 }
550             }
551 
552             return false;
553         }
554 
555         // ISO 8601 Universal time with sortable format
556         internal const string FileTimeFormat = "yyyy'-'MM'-'dd HH':'mm':'ss'.'fffffff";
557 
558         /// <summary>
559         /// Get the currently executing assembly path
560         /// </summary>
561         internal static string ExecutingAssemblyPath => Path.GetFullPath(AssemblyUtilities.GetAssemblyLocation(typeof(FileUtilities).GetTypeInfo().Assembly));
562 
563 
564         /// <summary>
565         /// Determines the full path for the given file-spec.
566         /// ASSUMES INPUT IS STILL ESCAPED
567         /// </summary>
568         /// <param name="fileSpec">The file spec to get the full path of.</param>
569         /// <param name="currentDirectory"></param>
570         /// <returns>full path</returns>
GetFullPath(string fileSpec, string currentDirectory)571         internal static string GetFullPath(string fileSpec, string currentDirectory)
572         {
573             // Sending data out of the engine into the filesystem, so time to unescape.
574             fileSpec = FixFilePath(EscapingUtilities.UnescapeAll(fileSpec));
575 
576             // Data coming back from the filesystem into the engine, so time to escape it back.
577             string fullPath = EscapingUtilities.Escape(NormalizePath(Path.Combine(currentDirectory, fileSpec)));
578 
579             if (NativeMethodsShared.IsWindows && !EndsWithSlash(fullPath))
580             {
581                 Match drive = FileUtilitiesRegex.DrivePattern.Match(fileSpec);
582                 Match UNCShare = FileUtilitiesRegex.UNCPattern.Match(fullPath);
583 
584                 if ((drive.Success && (drive.Length == fileSpec.Length)) ||
585                     (UNCShare.Success && (UNCShare.Length == fullPath.Length)))
586                 {
587                     // append trailing slash if Path.GetFullPath failed to (this happens with drive-specs and UNC shares)
588                     fullPath += Path.DirectorySeparatorChar;
589                 }
590             }
591 
592             return fullPath;
593         }
594 
595         /// <summary>
596         /// A variation of Path.GetFullPath that will return the input value
597         /// instead of throwing any IO exception.
598         /// Useful to get a better path for an error message, without the risk of throwing
599         /// if the error message was itself caused by the path being invalid!
600         /// </summary>
GetFullPathNoThrow(string path)601         internal static string GetFullPathNoThrow(string path)
602         {
603             try
604             {
605                 path = NormalizePath(path);
606             }
607             catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
608             {
609             }
610 
611             return path;
612         }
613 
614         /// <summary>
615         /// Compare if two paths, relative to the given currentDirectory are equal.
616         /// Does not throw IO exceptions. See <see cref="GetFullPathNoThrow(string)"/>
617         /// </summary>
618         /// <param name="first"></param>
619         /// <param name="second"></param>
620         /// <param name="currentDirectory"></param>
621         /// <returns></returns>
ComparePathsNoThrow(string first, string second, string currentDirectory)622         internal static bool ComparePathsNoThrow(string first, string second, string currentDirectory)
623         {
624             // perf: try comparing the bare strings first
625             if (string.Equals(first, second, PathComparison))
626             {
627                 return true;
628             }
629 
630             var firstFullPath = NormalizePathForComparisonNoThrow(first, currentDirectory);
631             var secondFullPath = NormalizePathForComparisonNoThrow(second, currentDirectory);
632 
633             return string.Equals(firstFullPath, secondFullPath, PathComparison);
634         }
635 
636         /// <summary>
637         /// Normalizes a path for path comparison
638         /// Does not throw IO exceptions. See <see cref="GetFullPathNoThrow(string)"/>
639         ///
640         /// </summary>
NormalizePathForComparisonNoThrow(string path, string currentDirectory)641         internal static string NormalizePathForComparisonNoThrow(string path, string currentDirectory)
642         {
643             // file is invalid, return early to avoid triggering an exception
644             if (PathIsInvalid(path))
645             {
646                 return path;
647             }
648 
649             var normalizedPath = path.NormalizeForPathComparison();
650             var fullPath = GetFullPathNoThrow(Path.Combine(currentDirectory, normalizedPath));
651 
652             return fullPath;
653         }
654 
PathIsInvalid(string path)655         internal static bool PathIsInvalid(string path)
656         {
657             if (path.IndexOfAny(InvalidPathChars) >= 0)
658             {
659                 return true;
660             }
661 
662             // Path.GetFileName does not react well to malformed filenames.
663             // For example, Path.GetFileName("a/b/foo:bar") returns bar instead of foo:bar
664             // It also throws exceptions on illegal path characters
665             var lastDirectorySeparator = path.LastIndexOfAny(Slashes);
666 
667             return path.IndexOfAny(InvalidFileNameChars, lastDirectorySeparator >= 0 ? lastDirectorySeparator + 1 : 0) >= 0;
668         }
669 
670 
671         /// <summary>
672         /// A variation on File.Delete that will throw ExceptionHandling.NotExpectedException exceptions
673         /// </summary>
DeleteNoThrow(string path)674         internal static void DeleteNoThrow(string path)
675         {
676             try
677             {
678                 File.Delete(FixFilePath(path));
679             }
680             catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
681             {
682             }
683         }
684 
685         /// <summary>
686         /// A variation on Directory.Delete that will throw ExceptionHandling.NotExpectedException exceptions
687         /// </summary>
688         [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Int32.TryParse(System.String,System.Int32@)", Justification = "We expect the out value to be 0 if the parse fails and compensate accordingly")]
DeleteDirectoryNoThrow(string path, bool recursive, int retryCount = 0, int retryTimeOut = 0)689         internal static void DeleteDirectoryNoThrow(string path, bool recursive, int retryCount = 0, int retryTimeOut = 0)
690         {
691             // Try parse will set the out parameter to 0 if the string passed in is null, or is outside the range of an int.
692             if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDDIRECTORYDELETERETRYCOUNT"), out retryCount))
693             {
694                 retryCount = 0;
695             }
696 
697             if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDDIRECTORYDELETRETRYTIMEOUT"), out retryTimeOut))
698             {
699                 retryTimeOut = 0;
700             }
701 
702             retryCount = retryCount < 1 ? 2 : retryCount;
703             retryTimeOut = retryTimeOut < 1 ? 500 : retryTimeOut;
704 
705             path = FixFilePath(path);
706 
707             for (int i = 0; i < retryCount; i++)
708             {
709                 try
710                 {
711                     if (Directory.Exists(path))
712                     {
713                         Directory.Delete(path, recursive);
714                         break;
715                     }
716                 }
717                 catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
718                 {
719                 }
720 
721                 if (i + 1 < retryCount) // should not wait for the final iteration since we not gonna check anyway
722                 {
723                     Thread.Sleep(retryTimeOut);
724                 }
725             }
726         }
727 
728         /// <summary>
729         /// Deletes a directory, ensuring that Directory.Delete does not get a path ending in a slash.
730         /// </summary>
731         /// <remarks>
732         /// This is a workaround for https://github.com/dotnet/corefx/issues/3780, which clashed with a common
733         /// pattern in our tests.
734         /// </remarks>
DeleteWithoutTrailingBackslash(string path, bool recursive = false)735         internal static void DeleteWithoutTrailingBackslash(string path, bool recursive = false)
736         {
737             //  Some tests (such as FileMatcher and Evaluation tests) were failing with an UnauthorizedAccessException or directory not empty.
738             //  This retry logic works around that issue.
739             const int NUM_TRIES = 3;
740             for (int i = 0; i < NUM_TRIES; i++)
741             {
742                 try
743                 {
744                     Directory.Delete(EnsureNoTrailingSlash(path), recursive);
745 
746                     //  If we got here, the directory was successfully deleted
747                     return;
748                 }
749                 catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
750                 {
751                     if (i == NUM_TRIES - 1)
752                     {
753                         //var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
754                         //string fileString = string.Join(Environment.NewLine, files);
755                         //string message = $"Unable to delete directory '{path}'.  Contents:" + Environment.NewLine + fileString;
756                         //throw new IOException(message, ex);
757                         throw;
758                     }
759                 }
760 
761                 Thread.Sleep(10);
762             }
763         }
764 
765         /// <summary>
766         /// A variation of Path.IsRooted that not throw any IO exception.
767         /// </summary>
IsRootedNoThrow(string path)768         internal static bool IsRootedNoThrow(string path)
769         {
770             bool result;
771 
772             try
773             {
774                 result = Path.IsPathRooted(FixFilePath(path));
775             }
776             catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
777             {
778                 result = false;
779             }
780 
781             return result;
782         }
783 
784         /// <summary>
785         /// Gets a file info object for the specified file path. If the file path
786         /// is invalid, or is a directory, or cannot be accessed, or does not exist,
787         /// it returns null rather than throwing or returning a FileInfo around a non-existent file.
788         /// This allows it to be called where File.Exists() (which never throws, and returns false
789         /// for directories) was called - but with the advantage that a FileInfo object is returned
790         /// that can be queried (e.g., for LastWriteTime) without hitting the disk again.
791         /// </summary>
792         /// <param name="filePath"></param>
793         /// <returns>FileInfo around path if it is an existing /file/, else null</returns>
GetFileInfoNoThrow(string filePath)794         internal static FileInfo GetFileInfoNoThrow(string filePath)
795         {
796             filePath = AttemptToShortenPath(filePath);
797 
798             FileInfo fileInfo;
799 
800             try
801             {
802                 fileInfo = new FileInfo(filePath);
803             }
804             catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
805             {
806                 // Invalid or inaccessible path: treat as if nonexistent file, just as File.Exists does
807                 return null;
808             }
809 
810             if (fileInfo.Exists)
811             {
812                 // It's an existing file
813                 return fileInfo;
814             }
815             else
816             {
817                 // Nonexistent, or existing but a directory, just as File.Exists behaves
818                 return null;
819             }
820         }
821 
822         /// <summary>
823         /// Returns if the directory exists
824         /// </summary>
825         /// <param name="fullPath">Full path to the directory in the filesystem</param>
826         /// <returns></returns>
DirectoryExistsNoThrow(string fullPath)827         internal static bool DirectoryExistsNoThrow(string fullPath)
828         {
829             fullPath = AttemptToShortenPath(fullPath);
830             if (NativeMethodsShared.IsWindows)
831             {
832                 NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA data = new NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA();
833                 bool success = false;
834 
835                 success = NativeMethodsShared.GetFileAttributesEx(fullPath, 0, ref data);
836                 if (success)
837                 {
838                     return ((data.fileAttributes & NativeMethodsShared.FILE_ATTRIBUTE_DIRECTORY) != 0);
839                 }
840 
841                 return false;
842             }
843             else
844             {
845                 try
846                 {
847                     return Directory.Exists(fullPath);
848                 }
849                 catch
850                 {
851                     return false;
852                 }
853             }
854         }
855 
856         /// <summary>
857         /// Returns if the directory exists
858         /// </summary>
859         /// <param name="fullPath">Full path to the file in the filesystem</param>
860         /// <returns></returns>
FileExistsNoThrow(string fullPath)861         internal static bool FileExistsNoThrow(string fullPath)
862         {
863             fullPath = AttemptToShortenPath(fullPath);
864             if (NativeMethodsShared.IsWindows)
865             {
866                 NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA data = new NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA();
867                 bool success = false;
868 
869                 success = NativeMethodsShared.GetFileAttributesEx(fullPath, 0, ref data);
870                 if (success)
871                 {
872                     return ((data.fileAttributes & NativeMethodsShared.FILE_ATTRIBUTE_DIRECTORY) == 0);
873                 }
874 
875                 return false;
876             }
877 
878             try
879             {
880                 return File.Exists(fullPath);
881             }
882             catch
883             {
884                 return false;
885             }
886         }
887 
888         /// <summary>
889         /// If there is a directory or file at the specified path, returns true.
890         /// Otherwise, returns false.
891         /// Does not throw IO exceptions, to match Directory.Exists and File.Exists.
892         /// Unlike calling each of those in turn it only accesses the disk once, which is faster.
893         /// </summary>
FileOrDirectoryExistsNoThrow(string fullPath)894         internal static bool FileOrDirectoryExistsNoThrow(string fullPath)
895         {
896             fullPath = AttemptToShortenPath(fullPath);
897             if (NativeMethodsShared.IsWindows)
898             {
899 #if !CLR2COMPATIBILITY
900                 if (Traits.Instance.CacheFileExistence)
901                 {
902                     // Possible future improvement: make sure file existence caching happens only at evaluation time, and maybe only within a build session. https://github.com/Microsoft/msbuild/issues/2306
903                     return FileExistenceCache.GetOrAdd(fullPath, NativeMethodsShared.FileExists);
904                 }
905                 else
906                 {
907 #endif
908                     return NativeMethodsShared.FileExists(fullPath);
909 #if !CLR2COMPATIBILITY
910                 }
911 #endif
912             }
913             else
914             {
915                 try
916                 {
917                     return File.Exists(fullPath) || Directory.Exists(fullPath);
918                 }
919                 catch
920                 {
921                     return false;
922                 }
923             }
924         }
925 
926         /// <summary>
927         /// This method returns true if the specified filename is a solution file (.sln), otherwise
928         /// it returns false.
929         /// </summary>
IsSolutionFilename(string filename)930         internal static bool IsSolutionFilename(string filename)
931         {
932             return HasExtension(filename, ".sln");
933         }
934 
935         /// <summary>
936         /// Returns true if the specified filename is a VC++ project file, otherwise returns false
937         /// </summary>
IsVCProjFilename(string filename)938         internal static bool IsVCProjFilename(string filename)
939         {
940             return HasExtension(filename, ".vcproj");
941         }
942 
943         /// <summary>
944         /// Returns true if the specified filename is a metaproject file (.metaproj), otherwise false.
945         /// </summary>
IsMetaprojectFilename(string filename)946         internal static bool IsMetaprojectFilename(string filename)
947         {
948             return HasExtension(filename, ".metaproj");
949         }
950 
IsBinaryLogFilename(string filename)951         internal static bool IsBinaryLogFilename(string filename)
952         {
953             return HasExtension(filename, ".binlog");
954         }
955 
HasExtension(string filename, string extension)956         private static bool HasExtension(string filename, string extension)
957         {
958             if (String.IsNullOrEmpty(filename))
959                 return false;
960 
961             return filename.EndsWith(extension, PathComparison);
962         }
963 
964         /// <summary>
965         /// Given the absolute location of a file, and a disc location, returns relative file path to that disk location.
966         /// Throws UriFormatException.
967         /// </summary>
968         /// <param name="basePath">
969         /// The base path we want to be relative to. Must be absolute.
970         /// Should <i>not</i> include a filename as the last segment will be interpreted as a directory.
971         /// </param>
972         /// <param name="path">
973         /// The path we need to make relative to basePath.  The path can be either absolute path or a relative path in which case it is relative to the base path.
974         /// If the path cannot be made relative to the base path (for example, it is on another drive), it is returned verbatim.
975         /// If the basePath is an empty string, returns the path.
976         /// </param>
977         /// <returns>relative path (can be the full path)</returns>
MakeRelative(string basePath, string path)978         internal static string MakeRelative(string basePath, string path)
979         {
980             ErrorUtilities.VerifyThrowArgumentNull(basePath, "basePath");
981             ErrorUtilities.VerifyThrowArgumentLength(path, "path");
982 
983             if (basePath.Length == 0)
984             {
985                 return path;
986             }
987 
988             Uri baseUri = new Uri(EnsureTrailingSlash(basePath), UriKind.Absolute); // May throw UriFormatException
989 
990             Uri pathUri = CreateUriFromPath(path);
991 
992             if (!pathUri.IsAbsoluteUri)
993             {
994                 // the path is already a relative url, we will just normalize it...
995                 pathUri = new Uri(baseUri, pathUri);
996             }
997 
998             Uri relativeUri = baseUri.MakeRelativeUri(pathUri);
999             string relativePath = Uri.UnescapeDataString(relativeUri.IsAbsoluteUri ? relativeUri.LocalPath : relativeUri.ToString());
1000 
1001             string result = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
1002 
1003             return result;
1004         }
1005 
1006         /// <summary>
1007         /// Helper function to create an Uri object from path.
1008         /// </summary>
1009         /// <param name="path">path string</param>
1010         /// <returns>uri object</returns>
CreateUriFromPath(string path)1011         private static Uri CreateUriFromPath(string path)
1012         {
1013             ErrorUtilities.VerifyThrowArgumentLength(path, "path");
1014 
1015             Uri pathUri = null;
1016 
1017             // Try absolute first, then fall back on relative, otherwise it
1018             // makes some absolute UNC paths like (\\foo\bar) relative ...
1019             if (!Uri.TryCreate(path, UriKind.Absolute, out pathUri))
1020             {
1021                 pathUri = new Uri(path, UriKind.Relative);
1022             }
1023 
1024             return pathUri;
1025         }
1026 
1027         /// <summary>
1028         /// Normalizes the path if and only if it is longer than max path,
1029         /// or would be if rooted by the current directory.
1030         /// This may make it shorter by removing ".."'s.
1031         /// </summary>
AttemptToShortenPath(string path)1032         internal static string AttemptToShortenPath(string path)
1033         {
1034             // >= not > because MAX_PATH assumes a trailing null
1035             if (path.Length >= NativeMethodsShared.MAX_PATH ||
1036                (!IsRootedNoThrow(path) && ((Directory.GetCurrentDirectory().Length + path.Length + 1 /* slash */) >= NativeMethodsShared.MAX_PATH)))
1037             {
1038                 // Attempt to make it shorter -- perhaps there are some \..\ elements
1039                 path = GetFullPathNoThrow(path);
1040             }
1041 
1042             return FixFilePath(path);
1043         }
1044 
1045         /// <summary>
1046         /// Get the folder N levels above the given. Will stop and return current path when rooted.
1047         /// </summary>
1048         /// <param name="path">Path to get the folder above.</param>
1049         /// <param name="count">Number of levels up to walk.</param>
1050         /// <returns>Full path to the folder N levels above the path.</returns>
GetFolderAbove(string path, int count = 1)1051         internal static string GetFolderAbove(string path, int count = 1)
1052         {
1053             if (count < 1)
1054                 return path;
1055 
1056             var parent = Directory.GetParent(path);
1057 
1058             while (count > 1 && parent?.Parent != null)
1059             {
1060                 parent = parent.Parent;
1061                 count--;
1062             }
1063 
1064             return parent?.FullName ?? path;
1065         }
1066 
1067         /// <summary>
1068         /// Combine multiple paths. Should only be used when compiling against .NET 2.0.
1069         /// <remarks>
1070         /// Only use in .NET 2.0. Otherwise, use System.IO.Path.Combine(...)
1071         /// </remarks>
1072         /// </summary>
1073         /// <param name="root">Root path.</param>
1074         /// <param name="paths">Paths to concatenate.</param>
1075         /// <returns>Combined path.</returns>
CombinePaths(string root, params string[] paths)1076         internal static string CombinePaths(string root, params string[] paths)
1077         {
1078             ErrorUtilities.VerifyThrowArgumentNull(root, nameof(root));
1079             ErrorUtilities.VerifyThrowArgumentNull(paths, nameof(paths));
1080 
1081             return paths.Aggregate(root, Path.Combine);
1082         }
1083 
TrimTrailingSlashes(this string s)1084         internal static string TrimTrailingSlashes(this string s)
1085         {
1086             return s.TrimEnd(Slashes);
1087         }
1088 
1089         /// <summary>
1090         /// Replace all backward slashes to forward slashes
1091         /// </summary>
ToSlash(this string s)1092         internal static string ToSlash(this string s)
1093         {
1094             return s.Replace('\\', '/');
1095         }
1096 
1097         /// <summary>
1098         /// Ensure all slashes are the current platform's slash
1099         /// </summary>
1100         /// <param name="s"></param>
1101         /// <returns></returns>
ToPlatformSlash(this string s)1102         internal static string ToPlatformSlash(this string s)
1103         {
1104             var separator = Path.DirectorySeparatorChar;
1105 
1106             return s.Replace(separator == '/' ? '\\' : '/', separator);
1107         }
1108 
WithTrailingSlash(this string s)1109         internal static string WithTrailingSlash(this string s)
1110         {
1111             return EnsureTrailingSlash(s);
1112         }
1113 
NormalizeForPathComparison(this string s)1114         internal static string NormalizeForPathComparison(this string s) => s.ToPlatformSlash().TrimTrailingSlashes();
1115 
1116         // TODO: assumption on file system case sensitivity: https://github.com/Microsoft/msbuild/issues/781
PathsEqual(string path1, string path2)1117         internal static bool PathsEqual(string path1, string path2)
1118         {
1119             if (path1 == null && path2 == null)
1120             {
1121                 return true;
1122             }
1123             if (path1 == null || path2 == null)
1124             {
1125                 return false;
1126             }
1127 
1128             var endA = path1.Length - 1;
1129             var endB = path2.Length - 1;
1130 
1131             // Trim trailing slashes
1132             for (var i = endA; i >= 0; i--)
1133             {
1134                 var c = path1[i];
1135                 if (c == '/' || c == '\\')
1136                 {
1137                     endA--;
1138                 }
1139                 else
1140                 {
1141                     break;
1142                 }
1143             }
1144 
1145             for (var i = endB; i >= 0; i--)
1146             {
1147                 var c = path2[i];
1148                 if (c == '/' || c == '\\')
1149                 {
1150                     endB--;
1151                 }
1152                 else
1153                 {
1154                     break;
1155                 }
1156             }
1157 
1158             if (endA != endB)
1159             {
1160                 // Lengths not the same
1161                 return false;
1162             }
1163 
1164             for (var i = 0; i <= endA; i++)
1165             {
1166                 var charA = (uint)path1[i];
1167                 var charB = (uint)path2[i];
1168 
1169                 if ((charA | charB) > 0x7F)
1170                 {
1171                     // Non-ascii chars move to non fast path
1172                     return PathsEqualNonAscii(path1, path2, i, endA - i + 1);
1173                 }
1174 
1175                 // uppercase both chars - notice that we need just one compare per char
1176                 if ((uint)(charA - 'a') <= (uint)('z' - 'a')) charA -= 0x20;
1177                 if ((uint)(charB - 'a') <= (uint)('z' - 'a')) charB -= 0x20;
1178 
1179                 // Set path delimiters the same
1180                 if (charA == '\\')
1181                 {
1182                     charA = '/';
1183                 }
1184                 if (charB == '\\')
1185                 {
1186                     charB = '/';
1187                 }
1188 
1189                 if (charA != charB)
1190                 {
1191                     return false;
1192                 }
1193             }
1194 
1195             return true;
1196         }
1197 
OpenWrite(string path, bool append, Encoding encoding = null)1198         internal static StreamWriter OpenWrite(string path, bool append, Encoding encoding = null)
1199         {
1200             const int DefaultFileStreamBufferSize = 4096;
1201             FileMode mode = append ? FileMode.Append : FileMode.Create;
1202             Stream fileStream = new FileStream(path, mode, FileAccess.Write, FileShare.Read, DefaultFileStreamBufferSize, FileOptions.SequentialScan);
1203             if (encoding == null)
1204             {
1205                 return new StreamWriter(fileStream);
1206             }
1207             else
1208             {
1209                 return new StreamWriter(fileStream, encoding);
1210             }
1211         }
1212 
OpenRead(string path, Encoding encoding = null, bool detectEncodingFromByteOrderMarks = true)1213         internal static StreamReader OpenRead(string path, Encoding encoding = null, bool detectEncodingFromByteOrderMarks = true)
1214         {
1215             const int DefaultFileStreamBufferSize = 4096;
1216             Stream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultFileStreamBufferSize, FileOptions.SequentialScan);
1217             if (encoding == null)
1218             {
1219                 return new StreamReader(fileStream);
1220             }
1221             else
1222             {
1223                 return new StreamReader(fileStream, encoding, detectEncodingFromByteOrderMarks);
1224             }
1225         }
1226 
1227         /// <summary>
1228         /// Locate a file in either the directory specified or a location in the
1229         /// directory structure above that directory.
1230         /// </summary>
GetDirectoryNameOfFileAbove(string startingDirectory, string fileName)1231         internal static string GetDirectoryNameOfFileAbove(string startingDirectory, string fileName)
1232         {
1233             // Canonicalize our starting location
1234             string lookInDirectory = Path.GetFullPath(startingDirectory);
1235 
1236             do
1237             {
1238                 // Construct the path that we will use to test against
1239                 string possibleFileDirectory = Path.Combine(lookInDirectory, fileName);
1240 
1241                 // If we successfully locate the file in the directory that we're
1242                 // looking in, simply return that location. Otherwise we'll
1243                 // keep moving up the tree.
1244                 if (File.Exists(possibleFileDirectory))
1245                 {
1246                     // We've found the file, return the directory we found it in
1247                     return lookInDirectory;
1248                 }
1249                 else
1250                 {
1251                     // GetDirectoryName will return null when we reach the root
1252                     // terminating our search
1253                     lookInDirectory = Path.GetDirectoryName(lookInDirectory);
1254                 }
1255             }
1256             while (lookInDirectory != null);
1257 
1258             // When we didn't find the location, then return an empty string
1259             return String.Empty;
1260         }
1261 
1262         /// <summary>
1263         /// Searches for a file based on the specified starting directory.
1264         /// </summary>
1265         /// <param name="file">The file to search for.</param>
1266         /// <param name="startingDirectory">An optional directory to start the search in.  The default location is the directory
1267         /// of the file containing the property function.</param>
1268         /// <returns>The full path of the file if it is found, otherwise an empty string.</returns>
GetPathOfFileAbove(string file, string startingDirectory)1269         internal static string GetPathOfFileAbove(string file, string startingDirectory)
1270         {
1271             // This method does not accept a path, only a file name
1272             if (file.Any(i => i.Equals(Path.DirectorySeparatorChar) || i.Equals(Path.AltDirectorySeparatorChar)))
1273             {
1274                 ErrorUtilities.ThrowArgument("InvalidGetPathOfFileAboveParameter", file);
1275             }
1276 
1277             // Search for a directory that contains that file
1278             string directoryName = GetDirectoryNameOfFileAbove(startingDirectory, file);
1279 
1280             return String.IsNullOrEmpty(directoryName) ? String.Empty : NormalizePath(directoryName, file);
1281         }
1282 
TryGetPathOfFileAbove(string file, string startingDirectory, out string fullPath)1283         internal static bool TryGetPathOfFileAbove(string file, string startingDirectory, out string fullPath)
1284         {
1285             fullPath = GetPathOfFileAbove(file, startingDirectory);
1286 
1287             return fullPath != String.Empty;
1288         }
1289 
1290         // Method is simple set of function calls and may inline;
1291         // we don't want it inlining into the tight loop that calls it as an exit case,
1292         // so mark as non-inlining
1293         [MethodImpl(MethodImplOptions.NoInlining)]
PathsEqualNonAscii(string strA, string strB, int i, int length)1294         private static bool PathsEqualNonAscii(string strA, string strB, int i, int length)
1295         {
1296             if (string.Compare(strA, i, strB, i, length, StringComparison.OrdinalIgnoreCase) == 0)
1297             {
1298                 return true;
1299             }
1300 
1301             var slash1 = strA.ToSlash();
1302             var slash2 = strB.ToSlash();
1303 
1304             if (string.Compare(slash1, i, slash2, i, length, StringComparison.OrdinalIgnoreCase) == 0)
1305             {
1306                 return true;
1307             }
1308 
1309             return false;
1310         }
1311 
1312 #if !CLR2COMPATIBILITY
1313         /// <summary>
1314         /// Clears the file existence cache.
1315         /// </summary>
ClearFileExistenceCache()1316         internal static void ClearFileExistenceCache()
1317         {
1318             FileExistenceCache.Clear();
1319         }
1320 #endif
1321     }
1322 }
1323