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