1 // Copyright (c) Microsoft. All rights reserved. 2 // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 4 using System; 5 using System.IO; 6 using System.Security; 7 using System.Collections; 8 using System.Diagnostics; 9 using System.Diagnostics.CodeAnalysis; 10 using System.Globalization; 11 using System.Text.RegularExpressions; 12 using System.Text; 13 using System.Threading; 14 using System.Runtime.InteropServices; 15 using System.Collections.Generic; 16 using Microsoft.Build.Collections; 17 using Microsoft.Build.Internal; 18 19 namespace Microsoft.Build.Shared 20 { 21 /// <summary> 22 /// This class contains utility methods for file IO. 23 /// </summary> 24 /// <comment> 25 /// Partial class in order to reduce the amount of sharing into different assemblies 26 /// </comment> 27 static internal partial class FileUtilities 28 { 29 /// <summary> 30 /// Encapsulates the definitions of the item-spec modifiers a.k.a. reserved item metadata. 31 /// </summary> 32 static internal class ItemSpecModifiers 33 { 34 #if DEBUG 35 /// <summary> 36 /// Whether to dump when a modifier is in the "wrong" (slow) casing 37 /// </summary> 38 private static readonly bool s_traceModifierCasing = (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDTRACEMODIFIERCASING"))); 39 #endif 40 41 // NOTE: If you add an item here that starts with a new letter, you need to update the case 42 // statements in IsItemSpecModifier and IsDerivableItemSpecModifier. 43 internal const string FullPath = "FullPath"; 44 internal const string RootDir = "RootDir"; 45 internal const string Filename = "Filename"; 46 internal const string Extension = "Extension"; 47 internal const string RelativeDir = "RelativeDir"; 48 internal const string Directory = "Directory"; 49 internal const string RecursiveDir = "RecursiveDir"; 50 internal const string Identity = "Identity"; 51 internal const string ModifiedTime = "ModifiedTime"; 52 internal const string CreatedTime = "CreatedTime"; 53 internal const string AccessedTime = "AccessedTime"; 54 internal const string DefiningProjectFullPath = "DefiningProjectFullPath"; 55 internal const string DefiningProjectDirectory = "DefiningProjectDirectory"; 56 internal const string DefiningProjectName = "DefiningProjectName"; 57 internal const string DefiningProjectExtension = "DefiningProjectExtension"; 58 59 // These are all the well-known attributes. 60 internal static readonly string[] All = 61 { 62 FullPath, 63 RootDir, 64 Filename, 65 Extension, 66 RelativeDir, 67 Directory, 68 RecursiveDir, // <-- Not derivable. 69 Identity, 70 ModifiedTime, 71 CreatedTime, 72 AccessedTime, 73 DefiningProjectFullPath, 74 DefiningProjectDirectory, 75 DefiningProjectName, 76 DefiningProjectExtension 77 }; 78 79 private static HashSet<string> s_tableOfItemSpecModifiers = new HashSet<string>(All, StringComparer.OrdinalIgnoreCase); 80 81 /// <summary> 82 /// Indicates if the given name is reserved for an item-spec modifier. 83 /// </summary> 84 [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Performance")] IsItemSpecModifier(string name)85 internal static bool IsItemSpecModifier(string name) 86 { 87 if (name == null) 88 { 89 return false; 90 } 91 92 93 /* 94 * What follows requires some explanation. 95 * 96 * This function is called many times and slowness here will be amplified 97 * in critical performance scenarios. 98 * 99 * The following switch statement attempts to identify item spec modifiers that 100 * have the exact case that our constants in ItemSpecModifiers have. This is the 101 * 99% case. 102 * 103 * Further, the switch statement can identify certain cases in which there is 104 * definitely no chance that 'name' is an item spec modifier. For example, a 105 * 7 letter 'name' that doesn't start with 'r' or 'R' can't be RootDir and 106 * therefore is not an item spec modifier. 107 * 108 */ 109 switch (name.Length) 110 { 111 case 7: // RootDir 112 switch (name[0]) 113 { 114 default: 115 return false; 116 case 'R': // RootDir 117 if (name == FileUtilities.ItemSpecModifiers.RootDir) 118 { 119 return true; 120 } 121 break; 122 case 'r': 123 break; 124 } 125 break; 126 case 8: // FullPath, Filename, Identity 127 128 switch (name[0]) 129 { 130 default: 131 return false; 132 case 'F': // Filename, FullPath 133 if (name == FileUtilities.ItemSpecModifiers.FullPath) 134 { 135 return true; 136 } 137 if (name == FileUtilities.ItemSpecModifiers.Filename) 138 { 139 return true; 140 } 141 break; 142 case 'f': 143 break; 144 case 'I': // Identity 145 if (name == FileUtilities.ItemSpecModifiers.Identity) 146 { 147 return true; 148 } 149 break; 150 case 'i': 151 break; 152 } 153 break; 154 case 9: // Extension, Directory 155 switch (name[0]) 156 { 157 default: 158 return false; 159 case 'D': // Directory 160 if (name == FileUtilities.ItemSpecModifiers.Directory) 161 { 162 return true; 163 } 164 break; 165 case 'd': 166 break; 167 case 'E': // Extension 168 if (name == FileUtilities.ItemSpecModifiers.Extension) 169 { 170 return true; 171 } 172 break; 173 case 'e': 174 break; 175 } 176 break; 177 case 11: // RelativeDir, CreatedTime 178 switch (name[0]) 179 { 180 default: 181 return false; 182 case 'C': // CreatedTime 183 if (name == FileUtilities.ItemSpecModifiers.CreatedTime) 184 { 185 return true; 186 } 187 break; 188 case 'c': 189 break; 190 case 'R': // RelativeDir 191 if (name == FileUtilities.ItemSpecModifiers.RelativeDir) 192 { 193 return true; 194 } 195 break; 196 case 'r': 197 break; 198 } 199 break; 200 case 12: // RecursiveDir, ModifiedTime, AccessedTime 201 202 switch (name[0]) 203 { 204 default: 205 return false; 206 case 'A': // AccessedTime 207 if (name == FileUtilities.ItemSpecModifiers.AccessedTime) 208 { 209 return true; 210 } 211 break; 212 case 'a': 213 break; 214 case 'M': // ModifiedTime 215 if (name == FileUtilities.ItemSpecModifiers.ModifiedTime) 216 { 217 return true; 218 } 219 break; 220 case 'm': 221 break; 222 case 'R': // RecursiveDir 223 if (name == FileUtilities.ItemSpecModifiers.RecursiveDir) 224 { 225 return true; 226 } 227 break; 228 case 'r': 229 break; 230 } 231 break; 232 case 19: 233 case 23: 234 case 24: 235 return IsDefiningProjectModifier(name); 236 default: 237 // Not the right length for a match. 238 return false; 239 } 240 241 // Could still be a case-insensitive match. 242 bool result = s_tableOfItemSpecModifiers.Contains(name); 243 244 #if DEBUG 245 if (result && s_traceModifierCasing) 246 { 247 Console.WriteLine("'{0}' is a non-standard casing. Replace the use with the standard casing like 'RecursiveDir' or 'FullPath' for a small performance improvement.", name); 248 } 249 #endif 250 251 return result; 252 } 253 254 /// <summary> 255 /// Indicates if the given name is reserved for one of the specific subset of itemspec 256 /// modifiers to do with the defining project of the item. 257 /// </summary> IsDefiningProjectModifier(string name)258 internal static bool IsDefiningProjectModifier(string name) 259 { 260 switch (name.Length) 261 { 262 case 19: // DefiningProjectName 263 if (name == FileUtilities.ItemSpecModifiers.DefiningProjectName) 264 { 265 return true; 266 } 267 break; 268 case 23: // DefiningProjectFullPath 269 if (name == FileUtilities.ItemSpecModifiers.DefiningProjectFullPath) 270 { 271 return true; 272 } 273 break; 274 case 24: // DefiningProjectDirectory, DefiningProjectExtension 275 276 switch (name[15]) 277 { 278 default: 279 return false; 280 case 'D': // DefiningProjectDirectory 281 if (name == FileUtilities.ItemSpecModifiers.DefiningProjectDirectory) 282 { 283 return true; 284 } 285 break; 286 case 'd': 287 break; 288 case 'E': // DefiningProjectExtension 289 if (name == FileUtilities.ItemSpecModifiers.DefiningProjectExtension) 290 { 291 return true; 292 } 293 break; 294 case 'e': 295 break; 296 } 297 break; 298 default: 299 return false; 300 } 301 302 // Could still be a case-insensitive match. 303 bool result = s_tableOfItemSpecModifiers.Contains(name); 304 305 #if DEBUG 306 if (result && s_traceModifierCasing) 307 { 308 Console.WriteLine("'{0}' is a non-standard casing. Replace the use with the standard casing like 'RecursiveDir' or 'FullPath' for a small performance improvement.", name); 309 } 310 #endif 311 312 return result; 313 } 314 315 /// <summary> 316 /// Indicates if the given name is reserved for a derivable item-spec modifier. 317 /// Derivable means it can be computed given a file name. 318 /// </summary> 319 /// <param name="name">Name to check.</param> 320 /// <returns>true, if name of a derivable modifier</returns> IsDerivableItemSpecModifier(string name)321 internal static bool IsDerivableItemSpecModifier(string name) 322 { 323 bool isItemSpecModifier = IsItemSpecModifier(name); 324 325 if (isItemSpecModifier) 326 { 327 if (name.Length == 12) 328 { 329 if (name[0] == 'R' || name[0] == 'r') 330 { 331 // The only 12 letter ItemSpecModifier that starts with 'R' is 'RecursiveDir' 332 return false; 333 } 334 } 335 } 336 337 return isItemSpecModifier; 338 } 339 340 /// <summary> 341 /// Performs path manipulations on the given item-spec as directed. 342 /// Does not cache the result. 343 /// </summary> GetItemSpecModifier(string currentDirectory, string itemSpec, string definingProjectEscaped, string modifier)344 internal static string GetItemSpecModifier(string currentDirectory, string itemSpec, string definingProjectEscaped, string modifier) 345 { 346 string dummy = null; 347 return GetItemSpecModifier(currentDirectory, itemSpec, definingProjectEscaped, modifier, ref dummy); 348 } 349 350 /// <summary> 351 /// Performs path manipulations on the given item-spec as directed. 352 /// 353 /// Supported modifiers: 354 /// %(FullPath) = full path of item 355 /// %(RootDir) = root directory of item 356 /// %(Filename) = item filename without extension 357 /// %(Extension) = item filename extension 358 /// %(RelativeDir) = item directory as given in item-spec 359 /// %(Directory) = full path of item directory relative to root 360 /// %(RecursiveDir) = portion of item path that matched a recursive wildcard 361 /// %(Identity) = item-spec as given 362 /// %(ModifiedTime) = last write time of item 363 /// %(CreatedTime) = creation time of item 364 /// %(AccessedTime) = last access time of item 365 /// 366 /// NOTES: 367 /// 1) This method always returns an empty string for the %(RecursiveDir) modifier because it does not have enough 368 /// information to compute it -- only the BuildItem class can compute this modifier. 369 /// 2) All but the file time modifiers could be cached, but it's not worth the space. Only full path is cached, as the others are just string manipulations. 370 /// </summary> 371 /// <remarks> 372 /// Methods of the Path class "normalize" slashes and periods. For example: 373 /// 1) successive slashes are combined into 1 slash 374 /// 2) trailing periods are discarded 375 /// 3) forward slashes are changed to back-slashes 376 /// 377 /// As a result, we cannot rely on any file-spec that has passed through a Path method to remain the same. We will 378 /// therefore not bother preserving slashes and periods when file-specs are transformed. 379 /// 380 /// Never returns null. 381 /// </remarks> 382 /// <param name="currentDirectory">The root directory for relative item-specs. When called on the Engine thread, this is the project directory. When called as part of building a task, it is null, indicating that the current directory should be used.</param> 383 /// <param name="itemSpec">The item-spec to modify.</param> 384 /// <param name="definingProjectEscaped">The path to the project that defined this item (may be null).</param> 385 /// <param name="modifier">The modifier to apply to the item-spec.</param> 386 /// <param name="fullPath">Full path if any was previously computed, to cache.</param> 387 /// <returns>The modified item-spec (can be empty string, but will never be null).</returns> 388 /// <exception cref="InvalidOperationException">Thrown when the item-spec is not a path.</exception> 389 [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Pre-existing")] GetItemSpecModifier(string currentDirectory, string itemSpec, string definingProjectEscaped, string modifier, ref string fullPath)390 internal static string GetItemSpecModifier(string currentDirectory, string itemSpec, string definingProjectEscaped, string modifier, ref string fullPath) 391 { 392 ErrorUtilities.VerifyThrow(itemSpec != null, "Need item-spec to modify."); 393 ErrorUtilities.VerifyThrow(modifier != null, "Need modifier to apply to item-spec."); 394 395 string modifiedItemSpec = null; 396 397 try 398 { 399 if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.FullPath, StringComparison.OrdinalIgnoreCase) == 0) 400 { 401 if (fullPath != null) 402 { 403 return fullPath; 404 } 405 406 if (currentDirectory == null) 407 { 408 currentDirectory = String.Empty; 409 } 410 411 modifiedItemSpec = GetFullPath(itemSpec, currentDirectory); 412 fullPath = modifiedItemSpec; 413 414 ThrowForUrl(modifiedItemSpec, itemSpec, currentDirectory); 415 } 416 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.RootDir, StringComparison.OrdinalIgnoreCase) == 0) 417 { 418 GetItemSpecModifier(currentDirectory, itemSpec, definingProjectEscaped, ItemSpecModifiers.FullPath, ref fullPath); 419 420 modifiedItemSpec = Path.GetPathRoot(fullPath); 421 422 if (!EndsWithSlash(modifiedItemSpec)) 423 { 424 ErrorUtilities.VerifyThrow(FileUtilitiesRegex.UNCPattern.IsMatch(modifiedItemSpec), 425 "Only UNC shares should be missing trailing slashes."); 426 427 // restore/append trailing slash if Path.GetPathRoot() has either removed it, or failed to add it 428 // (this happens with UNC shares) 429 modifiedItemSpec += Path.DirectorySeparatorChar; 430 } 431 } 432 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.Filename, StringComparison.OrdinalIgnoreCase) == 0) 433 { 434 // if the item-spec is a root directory, it can have no filename 435 if (Path.GetDirectoryName(itemSpec) == null) 436 { 437 // NOTE: this is to prevent Path.GetFileNameWithoutExtension() from treating server and share elements 438 // in a UNC file-spec as filenames e.g. \\server, \\server\share 439 modifiedItemSpec = String.Empty; 440 } 441 else 442 { 443 // Fix path to avoid problem with Path.GetFileNameWithoutExtension when backslashes in itemSpec on Unix 444 modifiedItemSpec = Path.GetFileNameWithoutExtension(FixFilePath(itemSpec)); 445 } 446 } 447 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.Extension, StringComparison.OrdinalIgnoreCase) == 0) 448 { 449 // if the item-spec is a root directory, it can have no extension 450 if (Path.GetDirectoryName(itemSpec) == null) 451 { 452 // NOTE: this is to prevent Path.GetExtension() from treating server and share elements in a UNC 453 // file-spec as filenames e.g. \\server.ext, \\server\share.ext 454 modifiedItemSpec = String.Empty; 455 } 456 else 457 { 458 modifiedItemSpec = Path.GetExtension(itemSpec); 459 } 460 } 461 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.RelativeDir, StringComparison.OrdinalIgnoreCase) == 0) 462 { 463 modifiedItemSpec = GetDirectory(itemSpec); 464 } 465 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.Directory, StringComparison.OrdinalIgnoreCase) == 0) 466 { 467 GetItemSpecModifier(currentDirectory, itemSpec, definingProjectEscaped, ItemSpecModifiers.FullPath, ref fullPath); 468 469 modifiedItemSpec = GetDirectory(fullPath); 470 471 if (NativeMethodsShared.IsWindows) 472 { 473 Match root = FileUtilitiesRegex.DrivePattern.Match(modifiedItemSpec); 474 475 if (!root.Success) 476 { 477 root = FileUtilitiesRegex.UNCPattern.Match(modifiedItemSpec); 478 } 479 480 if (root.Success) 481 { 482 ErrorUtilities.VerifyThrow((modifiedItemSpec.Length > root.Length) && IsSlash(modifiedItemSpec[root.Length]), 483 "Root directory must have a trailing slash."); 484 485 modifiedItemSpec = modifiedItemSpec.Substring(root.Length + 1); 486 } 487 } 488 else 489 { 490 ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(modifiedItemSpec) && IsSlash(modifiedItemSpec[0]), 491 "Expected a full non-windows path rooted at '/'."); 492 493 // A full unix path is always rooted at 494 // `/`, and a root-relative path is the 495 // rest of the string. 496 modifiedItemSpec = modifiedItemSpec.Substring(1); 497 } 498 } 499 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.RecursiveDir, StringComparison.OrdinalIgnoreCase) == 0) 500 { 501 // only the BuildItem class can compute this modifier -- so leave empty 502 modifiedItemSpec = String.Empty; 503 } 504 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.Identity, StringComparison.OrdinalIgnoreCase) == 0) 505 { 506 modifiedItemSpec = itemSpec; 507 } 508 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.ModifiedTime, StringComparison.OrdinalIgnoreCase) == 0) 509 { 510 // About to go out to the filesystem. This means data is leaving the engine, so need 511 // to unescape first. 512 string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec); 513 514 FileInfo info = FileUtilities.GetFileInfoNoThrow(unescapedItemSpec); 515 516 if (info != null) 517 { 518 modifiedItemSpec = info.LastWriteTime.ToString(FileTimeFormat, null); 519 } 520 else 521 { 522 // File does not exist, or path is a directory 523 modifiedItemSpec = String.Empty; 524 } 525 } 526 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.CreatedTime, StringComparison.OrdinalIgnoreCase) == 0) 527 { 528 // About to go out to the filesystem. This means data is leaving the engine, so need 529 // to unescape first. 530 string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec); 531 532 if (File.Exists(unescapedItemSpec)) 533 { 534 modifiedItemSpec = File.GetCreationTime(unescapedItemSpec).ToString(FileTimeFormat, null); 535 } 536 else 537 { 538 // File does not exist, or path is a directory 539 modifiedItemSpec = String.Empty; 540 } 541 } 542 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.AccessedTime, StringComparison.OrdinalIgnoreCase) == 0) 543 { 544 // About to go out to the filesystem. This means data is leaving the engine, so need 545 // to unescape first. 546 string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec); 547 548 if (File.Exists(unescapedItemSpec)) 549 { 550 modifiedItemSpec = File.GetLastAccessTime(unescapedItemSpec).ToString(FileTimeFormat, null); 551 } 552 else 553 { 554 // File does not exist, or path is a directory 555 modifiedItemSpec = String.Empty; 556 } 557 } 558 else if (IsDefiningProjectModifier(modifier)) 559 { 560 if (String.IsNullOrEmpty(definingProjectEscaped)) 561 { 562 // We have nothing to work with, but that's sometimes OK -- so just return String.Empty 563 modifiedItemSpec = String.Empty; 564 } 565 else 566 { 567 if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectDirectory, StringComparison.OrdinalIgnoreCase) == 0) 568 { 569 // ItemSpecModifiers.Directory does not contain the root directory 570 modifiedItemSpec = Path.Combine 571 ( 572 GetItemSpecModifier(currentDirectory, definingProjectEscaped, null, ItemSpecModifiers.RootDir), 573 GetItemSpecModifier(currentDirectory, definingProjectEscaped, null, ItemSpecModifiers.Directory) 574 ); 575 } 576 else 577 { 578 string additionalModifier = null; 579 580 if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectFullPath, StringComparison.OrdinalIgnoreCase) == 0) 581 { 582 additionalModifier = ItemSpecModifiers.FullPath; 583 } 584 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectName, StringComparison.OrdinalIgnoreCase) == 0) 585 { 586 additionalModifier = ItemSpecModifiers.Filename; 587 } 588 else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectExtension, StringComparison.OrdinalIgnoreCase) == 0) 589 { 590 additionalModifier = ItemSpecModifiers.Extension; 591 } 592 else 593 { 594 ErrorUtilities.ThrowInternalError("\"{0}\" is not a valid item-spec modifier.", modifier); 595 } 596 597 modifiedItemSpec = GetItemSpecModifier(currentDirectory, definingProjectEscaped, null, additionalModifier); 598 } 599 } 600 } 601 else 602 { 603 ErrorUtilities.ThrowInternalError("\"{0}\" is not a valid item-spec modifier.", modifier); 604 } 605 } 606 catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) 607 { 608 ErrorUtilities.VerifyThrowInvalidOperation(false, "Shared.InvalidFilespecForTransform", modifier, itemSpec, e.Message); 609 } 610 611 return modifiedItemSpec; 612 } 613 614 /// <summary> 615 /// Temporary check for something like http://foo which will end up like c:\foo\bar\http://foo 616 /// We should either have no colon, or exactly one colon. 617 /// UNDONE: This is a minimal safe change for Dev10. The correct fix should be to make GetFullPath/NormalizePath throw for this. 618 /// </summary> ThrowForUrl(string fullPath, string itemSpec, string currentDirectory)619 private static void ThrowForUrl(string fullPath, string itemSpec, string currentDirectory) 620 { 621 if (fullPath.IndexOf(':') != fullPath.LastIndexOf(':')) 622 { 623 // Cause a better error to appear 624 fullPath = Path.GetFullPath(Path.Combine(currentDirectory, itemSpec)); 625 } 626 } 627 } 628 } 629 } 630