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