|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Microsoft.Build.Shared.FileSystem;
#nullable disable
namespace Microsoft.Build.Shared
{
/// <summary>
/// This class contains utility methods for file IO.
/// </summary>
/// <comment>
/// Partial class in order to reduce the amount of sharing into different assemblies
/// </comment>
internal static partial class FileUtilities
{
/// <summary>
/// Encapsulates the definitions of the item-spec modifiers a.k.a. reserved item metadata.
/// </summary>
internal static class ItemSpecModifiers
{
#if DEBUG
/// <summary>
/// Whether to dump when a modifier is in the "wrong" (slow) casing
/// </summary>
private static readonly bool s_traceModifierCasing = (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDTRACEMODIFIERCASING")));
#endif
// NOTE: If you add an item here that starts with a new letter, you need to update the case
// statements in IsItemSpecModifier and IsDerivableItemSpecModifier.
internal const string FullPath = "FullPath";
internal const string RootDir = "RootDir";
internal const string Filename = "Filename";
internal const string Extension = "Extension";
internal const string RelativeDir = "RelativeDir";
internal const string Directory = "Directory";
internal const string RecursiveDir = "RecursiveDir";
internal const string Identity = "Identity";
internal const string ModifiedTime = "ModifiedTime";
internal const string CreatedTime = "CreatedTime";
internal const string AccessedTime = "AccessedTime";
internal const string DefiningProjectFullPath = "DefiningProjectFullPath";
internal const string DefiningProjectDirectory = "DefiningProjectDirectory";
internal const string DefiningProjectName = "DefiningProjectName";
internal const string DefiningProjectExtension = "DefiningProjectExtension";
// These are all the well-known attributes.
internal static readonly string[] All =
{
FullPath,
RootDir,
Filename,
Extension,
RelativeDir,
Directory,
RecursiveDir, // <-- Not derivable.
Identity,
ModifiedTime,
CreatedTime,
AccessedTime,
DefiningProjectFullPath,
DefiningProjectDirectory,
DefiningProjectName,
DefiningProjectExtension
};
private static HashSet<string> s_tableOfItemSpecModifiers = new HashSet<string>(All, StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Indicates if the given name is reserved for an item-spec modifier.
/// </summary>
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Performance")]
internal static bool IsItemSpecModifier(string name)
{
if (name == null)
{
return false;
}
/*
* What follows requires some explanation.
*
* This function is called many times and slowness here will be amplified
* in critical performance scenarios.
*
* The following switch statement attempts to identify item spec modifiers that
* have the exact case that our constants in ItemSpecModifiers have. This is the
* 99% case.
*
* Further, the switch statement can identify certain cases in which there is
* definitely no chance that 'name' is an item spec modifier. For example, a
* 7 letter 'name' that doesn't start with 'r' or 'R' can't be RootDir and
* therefore is not an item spec modifier.
*
*/
switch (name.Length)
{
case 7: // RootDir
switch (name[0])
{
default:
return false;
case 'R': // RootDir
if (name == FileUtilities.ItemSpecModifiers.RootDir)
{
return true;
}
break;
case 'r':
break;
}
break;
case 8: // FullPath, Filename, Identity
switch (name[0])
{
default:
return false;
case 'F': // Filename, FullPath
if (name == FileUtilities.ItemSpecModifiers.FullPath)
{
return true;
}
if (name == FileUtilities.ItemSpecModifiers.Filename)
{
return true;
}
break;
case 'f':
break;
case 'I': // Identity
if (name == FileUtilities.ItemSpecModifiers.Identity)
{
return true;
}
break;
case 'i':
break;
}
break;
case 9: // Extension, Directory
switch (name[0])
{
default:
return false;
case 'D': // Directory
if (name == FileUtilities.ItemSpecModifiers.Directory)
{
return true;
}
break;
case 'd':
break;
case 'E': // Extension
if (name == FileUtilities.ItemSpecModifiers.Extension)
{
return true;
}
break;
case 'e':
break;
}
break;
case 11: // RelativeDir, CreatedTime
switch (name[0])
{
default:
return false;
case 'C': // CreatedTime
if (name == FileUtilities.ItemSpecModifiers.CreatedTime)
{
return true;
}
break;
case 'c':
break;
case 'R': // RelativeDir
if (name == FileUtilities.ItemSpecModifiers.RelativeDir)
{
return true;
}
break;
case 'r':
break;
}
break;
case 12: // RecursiveDir, ModifiedTime, AccessedTime
switch (name[0])
{
default:
return false;
case 'A': // AccessedTime
if (name == FileUtilities.ItemSpecModifiers.AccessedTime)
{
return true;
}
break;
case 'a':
break;
case 'M': // ModifiedTime
if (name == FileUtilities.ItemSpecModifiers.ModifiedTime)
{
return true;
}
break;
case 'm':
break;
case 'R': // RecursiveDir
if (name == FileUtilities.ItemSpecModifiers.RecursiveDir)
{
return true;
}
break;
case 'r':
break;
}
break;
case 19:
case 23:
case 24:
return IsDefiningProjectModifier(name);
default:
// Not the right length for a match.
return false;
}
// Could still be a case-insensitive match.
bool result = s_tableOfItemSpecModifiers.Contains(name);
#if DEBUG
if (result && s_traceModifierCasing)
{
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);
}
#endif
return result;
}
/// <summary>
/// Indicates if the given name is reserved for one of the specific subset of itemspec
/// modifiers to do with the defining project of the item.
/// </summary>
internal static bool IsDefiningProjectModifier(string name)
{
switch (name.Length)
{
case 19: // DefiningProjectName
if (name == FileUtilities.ItemSpecModifiers.DefiningProjectName)
{
return true;
}
break;
case 23: // DefiningProjectFullPath
if (name == FileUtilities.ItemSpecModifiers.DefiningProjectFullPath)
{
return true;
}
break;
case 24: // DefiningProjectDirectory, DefiningProjectExtension
switch (name[15])
{
default:
return false;
case 'D': // DefiningProjectDirectory
if (name == FileUtilities.ItemSpecModifiers.DefiningProjectDirectory)
{
return true;
}
break;
case 'd':
break;
case 'E': // DefiningProjectExtension
if (name == FileUtilities.ItemSpecModifiers.DefiningProjectExtension)
{
return true;
}
break;
case 'e':
break;
}
break;
default:
return false;
}
// Could still be a case-insensitive match.
bool result = s_tableOfItemSpecModifiers.Contains(name);
#if DEBUG
if (result && s_traceModifierCasing)
{
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);
}
#endif
return result;
}
/// <summary>
/// Indicates if the given name is reserved for a derivable item-spec modifier.
/// Derivable means it can be computed given a file name.
/// </summary>
/// <param name="name">Name to check.</param>
/// <returns>true, if name of a derivable modifier</returns>
internal static bool IsDerivableItemSpecModifier(string name)
{
bool isItemSpecModifier = IsItemSpecModifier(name);
if (isItemSpecModifier)
{
if (name.Length == 12)
{
if (name[0] == 'R' || name[0] == 'r')
{
// The only 12 letter ItemSpecModifier that starts with 'R' is 'RecursiveDir'
return false;
}
}
}
return isItemSpecModifier;
}
/// <summary>
/// Performs path manipulations on the given item-spec as directed.
/// Does not cache the result.
/// </summary>
internal static string GetItemSpecModifier(string currentDirectory, string itemSpec, string definingProjectEscaped, string modifier)
{
string dummy = null;
return GetItemSpecModifier(currentDirectory, itemSpec, definingProjectEscaped, modifier, ref dummy);
}
/// <summary>
/// Performs path manipulations on the given item-spec as directed.
///
/// Supported modifiers:
/// %(FullPath) = full path of item
/// %(RootDir) = root directory of item
/// %(Filename) = item filename without extension
/// %(Extension) = item filename extension
/// %(RelativeDir) = item directory as given in item-spec
/// %(Directory) = full path of item directory relative to root
/// %(RecursiveDir) = portion of item path that matched a recursive wildcard
/// %(Identity) = item-spec as given
/// %(ModifiedTime) = last write time of item
/// %(CreatedTime) = creation time of item
/// %(AccessedTime) = last access time of item
///
/// NOTES:
/// 1) This method always returns an empty string for the %(RecursiveDir) modifier because it does not have enough
/// information to compute it -- only the BuildItem class can compute this modifier.
/// 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.
/// </summary>
/// <remarks>
/// Methods of the Path class "normalize" slashes and periods. For example:
/// 1) successive slashes are combined into 1 slash
/// 2) trailing periods are discarded
/// 3) forward slashes are changed to back-slashes
///
/// As a result, we cannot rely on any file-spec that has passed through a Path method to remain the same. We will
/// therefore not bother preserving slashes and periods when file-specs are transformed.
///
/// Never returns null.
/// </remarks>
/// <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>
/// <param name="itemSpec">The item-spec to modify.</param>
/// <param name="definingProjectEscaped">The path to the project that defined this item (may be null).</param>
/// <param name="modifier">The modifier to apply to the item-spec.</param>
/// <param name="fullPath">Full path if any was previously computed, to cache.</param>
/// <returns>The modified item-spec (can be empty string, but will never be null).</returns>
/// <exception cref="InvalidOperationException">Thrown when the item-spec is not a path.</exception>
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Pre-existing")]
internal static string GetItemSpecModifier(string currentDirectory, string itemSpec, string definingProjectEscaped, string modifier, ref string fullPath)
{
ErrorUtilities.VerifyThrow(itemSpec != null, "Need item-spec to modify.");
ErrorUtilities.VerifyThrow(modifier != null, "Need modifier to apply to item-spec.");
string modifiedItemSpec = null;
try
{
if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.FullPath, StringComparison.OrdinalIgnoreCase))
{
if (fullPath != null)
{
return fullPath;
}
if (currentDirectory == null)
{
currentDirectory = String.Empty;
}
modifiedItemSpec = GetFullPath(itemSpec, currentDirectory);
fullPath = modifiedItemSpec;
ThrowForUrl(modifiedItemSpec, itemSpec, currentDirectory);
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.RootDir, StringComparison.OrdinalIgnoreCase))
{
GetItemSpecModifier(currentDirectory, itemSpec, definingProjectEscaped, ItemSpecModifiers.FullPath, ref fullPath);
modifiedItemSpec = Path.GetPathRoot(fullPath);
if (!EndsWithSlash(modifiedItemSpec))
{
ErrorUtilities.VerifyThrow(FileUtilitiesRegex.StartsWithUncPattern(modifiedItemSpec),
"Only UNC shares should be missing trailing slashes.");
// restore/append trailing slash if Path.GetPathRoot() has either removed it, or failed to add it
// (this happens with UNC shares)
modifiedItemSpec += Path.DirectorySeparatorChar;
}
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.Filename, StringComparison.OrdinalIgnoreCase))
{
// if the item-spec is a root directory, it can have no filename
if (IsRootDirectory(itemSpec))
{
// NOTE: this is to prevent Path.GetFileNameWithoutExtension() from treating server and share elements
// in a UNC file-spec as filenames e.g. \\server, \\server\share
modifiedItemSpec = String.Empty;
}
else
{
// Fix path to avoid problem with Path.GetFileNameWithoutExtension when backslashes in itemSpec on Unix
modifiedItemSpec = Path.GetFileNameWithoutExtension(FixFilePath(itemSpec));
}
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.Extension, StringComparison.OrdinalIgnoreCase))
{
// if the item-spec is a root directory, it can have no extension
if (IsRootDirectory(itemSpec))
{
// NOTE: this is to prevent Path.GetExtension() from treating server and share elements in a UNC
// file-spec as filenames e.g. \\server.ext, \\server\share.ext
modifiedItemSpec = String.Empty;
}
else
{
modifiedItemSpec = Path.GetExtension(itemSpec);
}
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.RelativeDir, StringComparison.OrdinalIgnoreCase))
{
modifiedItemSpec = GetDirectory(itemSpec);
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.Directory, StringComparison.OrdinalIgnoreCase))
{
GetItemSpecModifier(currentDirectory, itemSpec, definingProjectEscaped, ItemSpecModifiers.FullPath, ref fullPath);
modifiedItemSpec = GetDirectory(fullPath);
if (NativeMethodsShared.IsWindows)
{
int length = -1;
if (FileUtilitiesRegex.StartsWithDrivePattern(modifiedItemSpec))
{
length = 2;
}
else
{
length = FileUtilitiesRegex.StartsWithUncPatternMatchLength(modifiedItemSpec);
}
if (length != -1)
{
ErrorUtilities.VerifyThrow((modifiedItemSpec.Length > length) && IsSlash(modifiedItemSpec[length]),
"Root directory must have a trailing slash.");
modifiedItemSpec = modifiedItemSpec.Substring(length + 1);
}
}
else
{
ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(modifiedItemSpec) && IsSlash(modifiedItemSpec[0]),
"Expected a full non-windows path rooted at '/'.");
// A full unix path is always rooted at
// `/`, and a root-relative path is the
// rest of the string.
modifiedItemSpec = modifiedItemSpec.Substring(1);
}
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.RecursiveDir, StringComparison.OrdinalIgnoreCase))
{
// only the BuildItem class can compute this modifier -- so leave empty
modifiedItemSpec = String.Empty;
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.Identity, StringComparison.OrdinalIgnoreCase))
{
modifiedItemSpec = itemSpec;
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.ModifiedTime, StringComparison.OrdinalIgnoreCase))
{
// About to go out to the filesystem. This means data is leaving the engine, so need
// to unescape first.
string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec);
FileInfo info = FileUtilities.GetFileInfoNoThrow(unescapedItemSpec);
if (info != null)
{
modifiedItemSpec = info.LastWriteTime.ToString(FileTimeFormat, null);
}
else
{
// File does not exist, or path is a directory
modifiedItemSpec = String.Empty;
}
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.CreatedTime, StringComparison.OrdinalIgnoreCase))
{
// About to go out to the filesystem. This means data is leaving the engine, so need
// to unescape first.
string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec);
if (FileSystems.Default.FileExists(unescapedItemSpec))
{
modifiedItemSpec = File.GetCreationTime(unescapedItemSpec).ToString(FileTimeFormat, null);
}
else
{
// File does not exist, or path is a directory
modifiedItemSpec = String.Empty;
}
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.AccessedTime, StringComparison.OrdinalIgnoreCase))
{
// About to go out to the filesystem. This means data is leaving the engine, so need
// to unescape first.
string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec);
if (FileSystems.Default.FileExists(unescapedItemSpec))
{
modifiedItemSpec = File.GetLastAccessTime(unescapedItemSpec).ToString(FileTimeFormat, null);
}
else
{
// File does not exist, or path is a directory
modifiedItemSpec = String.Empty;
}
}
else if (IsDefiningProjectModifier(modifier))
{
if (String.IsNullOrEmpty(definingProjectEscaped))
{
// We have nothing to work with, but that's sometimes OK -- so just return String.Empty
modifiedItemSpec = String.Empty;
}
else
{
if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectDirectory, StringComparison.OrdinalIgnoreCase))
{
// ItemSpecModifiers.Directory does not contain the root directory
modifiedItemSpec = Path.Combine(
GetItemSpecModifier(currentDirectory, definingProjectEscaped, null, ItemSpecModifiers.RootDir),
GetItemSpecModifier(currentDirectory, definingProjectEscaped, null, ItemSpecModifiers.Directory));
}
else
{
string additionalModifier = null;
if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectFullPath, StringComparison.OrdinalIgnoreCase))
{
additionalModifier = ItemSpecModifiers.FullPath;
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectName, StringComparison.OrdinalIgnoreCase))
{
additionalModifier = ItemSpecModifiers.Filename;
}
else if (string.Equals(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectExtension, StringComparison.OrdinalIgnoreCase))
{
additionalModifier = ItemSpecModifiers.Extension;
}
else
{
ErrorUtilities.ThrowInternalError("\"{0}\" is not a valid item-spec modifier.", modifier);
}
modifiedItemSpec = GetItemSpecModifier(currentDirectory, definingProjectEscaped, null, additionalModifier);
}
}
}
else
{
ErrorUtilities.ThrowInternalError("\"{0}\" is not a valid item-spec modifier.", modifier);
}
}
catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
{
ErrorUtilities.ThrowInvalidOperation("Shared.InvalidFilespecForTransform", modifier, itemSpec, e.Message);
}
return modifiedItemSpec;
}
/// <summary>
/// Indicates whether the given path is a UNC or drive pattern root directory.
/// <para>Note: This function mimics the behavior of checking if Path.GetDirectoryName(path) == null.</para>
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
private static bool IsRootDirectory(string path)
{
// Eliminate all non-rooted paths
if (!Path.IsPathRooted(path))
{
return false;
}
int uncMatchLength = FileUtilitiesRegex.StartsWithUncPatternMatchLength(path);
// Determine if the given path is a standard drive/unc pattern root
if (FileUtilitiesRegex.IsDrivePattern(path) ||
FileUtilitiesRegex.IsDrivePatternWithSlash(path) ||
uncMatchLength == path.Length)
{
return true;
}
// Eliminate all non-root unc paths.
if (uncMatchLength != -1)
{
return false;
}
// Eliminate any drive patterns that don't have a slash after the colon or where the 4th character is a non-slash
// A non-slash at [3] is specifically checked here because Path.GetDirectoryName
// considers "C:///" a valid root.
if (FileUtilitiesRegex.StartsWithDrivePattern(path) &&
((path.Length >= 3 && path[2] != '\\' && path[2] != '/') ||
(path.Length >= 4 && path[3] != '\\' && path[3] != '/')))
{
return false;
}
// There are some edge cases that can get to this point.
// After eliminating valid / invalid roots, fall back on original behavior.
return Path.GetDirectoryName(path) == null;
}
/// <summary>
/// Temporary check for something like http://foo which will end up like c:\foo\bar\http://foo
/// We should either have no colon, or exactly one colon.
/// UNDONE: This is a minimal safe change for Dev10. The correct fix should be to make GetFullPath/NormalizePath throw for this.
/// </summary>
private static void ThrowForUrl(string fullPath, string itemSpec, string currentDirectory)
{
if (fullPath.IndexOf(':') != fullPath.LastIndexOf(':'))
{
// Cause a better error to appear
fullPath = Path.GetFullPath(Path.Combine(currentDirectory, itemSpec));
}
}
}
}
}
|