// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
#if NET
using System.Buffers;
#endif
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
#if NETFRAMEWORK
using NewPath = Microsoft.IO.Path;
using Path = System.IO.Path;
#else
using NewPath = System.IO.Path;
using Path = System.IO.Path;
#endif
namespace Microsoft.Build.Framework
{
/// <summary>
/// This class contains utility methods for file IO.
/// Functions from FileUtilities are transferred here as part of the effort to remove Shared files.
/// </summary>
internal static partial class FileUtilities
{
private const char UnixDirectorySeparator = '/';
private const char WindowsDirectorySeparator = '\\';
internal static readonly char[] Slashes = [UnixDirectorySeparator, WindowsDirectorySeparator];
/// <summary>
/// AsyncLocal working directory for use during property/item expansion in multithreaded mode.
/// Set by MultiThreadedTaskEnvironmentDriver when building projects. null in multi-process mode.
/// Using AsyncLocal ensures the value flows to child threads/tasks spawned during execution of tasks.
/// </summary>
private static readonly AsyncLocal<string?> s_currentThreadWorkingDirectory = new();
internal static string? CurrentThreadWorkingDirectory
{
get => s_currentThreadWorkingDirectory.Value;
set => s_currentThreadWorkingDirectory.Value = value;
}
/// <summary>
/// The directory where MSBuild stores cache information used during the build.
/// </summary>
internal static string? cacheDirectory = null;
/// <summary>
/// FOR UNIT TESTS ONLY
/// Clear out the static variable used for the cache directory so that tests that
/// modify it can validate their modifications.
/// </summary>
internal static void ClearCacheDirectoryPath()
{
cacheDirectory = null;
}
public static readonly StringComparison PathComparison = IsFileSystemCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
public static readonly StringComparer PathComparer = IsFileSystemCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
private static bool? s_isFileSystemCaseSensitive;
public static bool IsFileSystemCaseSensitive
=> s_isFileSystemCaseSensitive ??= ComputeIsFileSystemCaseSensitive();
/// <summary>
/// Determines whether the file system is case sensitive.
/// Copied from https://github.com/dotnet/runtime/blob/73ba11f3015216b39cb866d9fb7d3d25e93489f2/src/libraries/Common/src/System/IO/PathInternal.CaseSensitivity.cs#L41-L59.
/// </summary>
private static bool ComputeIsFileSystemCaseSensitive()
{
try
{
string pathWithUpperCase = Path.Combine(Path.GetTempPath(), $"CASESENSITIVETEST{Guid.NewGuid():N}");
using (new FileStream(pathWithUpperCase, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 0x1000, FileOptions.DeleteOnClose))
{
string lowerCased = pathWithUpperCase.ToLowerInvariant();
return !FileSystems.Default.FileExists(lowerCased);
}
}
catch (Exception ex)
{
// In case something goes terribly wrong, we don't want to fail just because
// of a casing test, so we assume case-insensitive-but-preserving.
Debug.Fail($"Casing test failed: {ex}");
return false;
}
}
/// <summary>
/// Copied from https://github.com/dotnet/corefx/blob/056715ff70e14712419d82d51c8c50c54b9ea795/src/Common/src/System/IO/PathInternal.Windows.cs#L61
/// MSBuild should support the union of invalid path chars across the supported OSes, so builds can have the same behaviour crossplatform: https://github.com/dotnet/msbuild/issues/781#issuecomment-243942514
/// </summary>
#if NET
internal static readonly SearchValues<char> InvalidPathChars = SearchValues.Create(
#else
internal static readonly char[] InvalidPathChars = (
#endif
[
'|', '\0',
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
(char)31
]);
/// <summary>
/// Copied from https://github.com/dotnet/corefx/blob/387cf98c410bdca8fd195b28cbe53af578698f94/src/System.Runtime.Extensions/src/System/IO/Path.Windows.cs#L18
/// MSBuild should support the union of invalid path chars across the supported OSes, so builds can have the same behaviour crossplatform: https://github.com/dotnet/msbuild/issues/781#issuecomment-243942514
/// </summary>
internal static readonly char[] InvalidFileNameCharsArray =
[
'\"', '<', '>', '|', '\0',
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
(char)31, ':', '*', '?', '\\', '/'
];
#if NET
internal static readonly SearchValues<char> InvalidFileNameChars = SearchValues.Create(InvalidFileNameCharsArray);
#else
internal static char[] InvalidFileNameChars => InvalidFileNameCharsArray;
#endif
internal static readonly string DirectorySeparatorString = Path.DirectorySeparatorChar.ToString();
private static readonly ConcurrentDictionary<string, bool> FileExistenceCache = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
private static readonly IFileSystem DefaultFileSystem = FileSystems.Default;
/// <summary>
/// Retrieves the MSBuild runtime cache directory
/// </summary>
internal static string GetCacheDirectory()
{
if (cacheDirectory == null)
{
cacheDirectory = Path.Combine(TempFileDirectory, string.Format(CultureInfo.CurrentUICulture, "MSBuild{0}-{1}", EnvironmentUtilities.CurrentProcessId, AppDomain.CurrentDomain.Id));
}
return cacheDirectory;
}
/// <summary>
/// Clears the MSBuild runtime cache
/// </summary>
internal static void ClearCacheDirectory()
{
string cacheDirectory = GetCacheDirectory();
if (DefaultFileSystem.DirectoryExists(cacheDirectory))
{
DeleteDirectoryNoThrow(cacheDirectory, true);
}
}
/// <summary>
/// Indicates if the given character is a slash in current OS.
/// </summary>
/// <param name="c"></param>
/// <returns>true, if slash</returns>
internal static bool IsSlash(char c)
{
return (c == Path.DirectorySeparatorChar) || (c == Path.AltDirectorySeparatorChar);
}
/// <summary>
/// Indicates if the given file-spec ends with a slash.
/// </summary>
/// <param name="fileSpec">The file spec.</param>
/// <returns>true, if file-spec has trailing slash</returns>
internal static bool EndsWithSlash(string fileSpec)
{
return (fileSpec.Length > 0)
? IsSlash(fileSpec[fileSpec.Length - 1])
: false;
}
/// <summary>
/// Fixes backslashes to forward slashes on Unix. This allows to recognise windows style paths on Unix.
/// However, this leads to incorrect path on Linux if backslash was part of the file/directory name.
/// </summary>
internal static string FixFilePath(string path)
{
return string.IsNullOrEmpty(path) || Path.DirectorySeparatorChar == WindowsDirectorySeparator ? path : path.Replace(WindowsDirectorySeparator, UnixDirectorySeparator);
}
/// <summary>
/// If the given path doesn't have a trailing slash then add one.
/// If the path is an empty string, does not modify it.
/// </summary>
/// <param name="fileSpec">The path to check.</param>
/// <returns>A path with a slash.</returns>
internal static string EnsureTrailingSlash(string fileSpec)
{
fileSpec = FixFilePath(fileSpec);
if (fileSpec.Length > 0 && !IsSlash(fileSpec[fileSpec.Length - 1]))
{
fileSpec += Path.DirectorySeparatorChar;
}
return fileSpec;
}
/// <summary>
/// Ensures the path does not have a trailing slash.
/// </summary>
internal static string EnsureNoTrailingSlash(string path)
{
path = FixFilePath(path);
if (path.Length > 0 && IsSlash(path[path.Length - 1]))
{
path = path.Substring(0, path.Length - 1);
}
return path;
}
public static bool IsPathTooLong(string path)
=> path.Length >= NativeMethods.MaxPath; // >= not > because MAX_PATH assumes a trailing null
/// <summary>
/// Checks if the path contains backslashes on Unix.
/// </summary>
private static bool HasWindowsDirectorySeparatorOnUnix(string path)
=> NativeMethods.IsUnixLike && path.IndexOf(WindowsDirectorySeparator) >= 0;
/// <summary>
/// Checks if the path contains forward slashes on Windows.
/// </summary>
private static bool HasUnixDirectorySeparatorOnWindows(string path)
=> NativeMethods.IsWindows && path.IndexOf(UnixDirectorySeparator) >= 0;
/// <summary>
/// Quickly checks if the path may contain relative segments like "." or "..".
/// This is a non-precise detection that may have false positives but no false negatives.
/// </summary>
/// <remarks>
/// Check for relative path segments "." and ".."
/// In absolute path those segments can not appear in the beginning of the path, only after a path separator.
/// This is not a precise full detection of relative segments. There are no false negatives as this might affect correctness, but it may have false positives:
/// like when there is a hidden file or directory starting with a dot, or on linux the backslash and dot can be part of the file name.
/// </remarks>
private static bool MayHaveRelativeSegment(string path)
=> path.Contains("/.") || path.Contains("\\.");
/// <summary>
/// If the given path doesn't have a trailing slash then add one.
/// </summary>
/// <param name="path">The absolute path to check.</param>
/// <returns>An absolute path with a trailing slash.</returns>
/// <remarks>
/// If the path does not require modification, returns the current instance to avoid unnecessary allocations.
/// Preserves the OriginalValue of the current instance.
/// </remarks>
internal static AbsolutePath EnsureTrailingSlash(AbsolutePath path)
{
if (string.IsNullOrEmpty(path.Value))
{
return path;
}
// Check if the path already has a trailing slash and no separator fixing is needed on Unix.
// EnsureTrailingSlash should also fix the path separators on Unix.
if (IsSlash(path.Value[path.Value.Length - 1]) && !HasWindowsDirectorySeparatorOnUnix(path.Value))
{
return path;
}
return new AbsolutePath(EnsureTrailingSlash(path.Value),
original: path.OriginalValue,
ignoreRootedCheck: true);
}
/// <summary>
/// Ensures the absolute path does not have a trailing slash.
/// </summary>
/// <param name="path">The absolute path to check.</param>
/// <returns>An absolute path without a trailing slash.</returns>
/// <remarks>
/// If the path does not require modification, returns the current instance to avoid unnecessary allocations.
/// Preserves the OriginalValue of the current instance.
/// </remarks>
internal static AbsolutePath EnsureNoTrailingSlash(AbsolutePath path)
{
if (string.IsNullOrEmpty(path.Value))
{
return path;
}
// Check if already has no trailing slash and no separator fixing needed on unix
// (EnsureNoTrailingSlash also should fix the paths on unix).
if (!IsSlash(path.Value[path.Value.Length - 1]) && !HasWindowsDirectorySeparatorOnUnix(path.Value))
{
return path;
}
return new AbsolutePath(EnsureNoTrailingSlash(path.Value),
original: path.OriginalValue,
ignoreRootedCheck: true);
}
/// <summary>
/// Gets the canonicalized full path of the provided path.
/// Resolves relative segments like "." and "..". Fixes directory separators.
/// ASSUMES INPUT IS ALREADY UNESCAPED.
/// </summary>
/// <remarks>
/// If the path does not require modification, returns the current instance to avoid unnecessary allocations.
/// Preserves the OriginalValue of the current instance.
/// </remarks>
internal static AbsolutePath NormalizePath(AbsolutePath path)
{
if (string.IsNullOrEmpty(path.Value))
{
return path;
}
if (!MayHaveRelativeSegment(path.Value) &&
!HasWindowsDirectorySeparatorOnUnix(path.Value) &&
!HasUnixDirectorySeparatorOnWindows(path.Value))
{
return path;
}
return new AbsolutePath(FixFilePath(NewPath.GetFullPath(path.Value)),
original: path.OriginalValue,
ignoreRootedCheck: true);
}
/// <summary>
/// Fixes file path separators for the current platform.
/// </summary>
internal static AbsolutePath FixFilePath(AbsolutePath path)
{
if (string.IsNullOrEmpty(path.Value) || !HasWindowsDirectorySeparatorOnUnix(path.Value))
{
return path;
}
return new AbsolutePath(FixFilePath(path.Value),
original: path.OriginalValue,
ignoreRootedCheck: true);
}
/// <summary>
/// If <see cref="CurrentThreadWorkingDirectory"/> is set and <paramref name="path"/> is relative,
/// resolves it to an <see cref="AbsolutePath"/> using the thread-local working directory as base.
/// Returns <c>null</c> when the path does not need resolution (no thread working directory,
/// empty path, or already fully qualified).
/// </summary>
/// <remarks>
/// <para>
/// Callers are responsible for calling <see cref="FixFilePath(string)"/>
/// BEFORE passing the path to this method. On Linux, backslash is a valid filename character
/// and Path.Combine/GetFullPath won't recognize ..\segments unless backslashes are first
/// normalized to forward slashes.
/// </para>
/// <para>
/// GetFullPath can throw for inputs with illegal path characters (e.g., wildcards).
/// In that case we fall back to a simple Path.Combine, which preserves the non-throwing
/// behavior of APIs like File.Exists and Directory.Exists.
/// </para>
/// </remarks>
internal static AbsolutePath? MakeFullPathFromThreadWorkingDirectory(string path)
{
string? workingDir = CurrentThreadWorkingDirectory;
if (string.IsNullOrEmpty(workingDir) || string.IsNullOrEmpty(path))
{
return null;
}
if (NewPath.IsPathFullyQualified(path))
{
return null;
}
// Use the 2-argument GetFullPath overload — it correctly handles drive-relative
// ("\foo") and current-directory-relative ("C:foo") paths on Windows by resolving
// them against the specified base directory, not the process CWD.
try
{
return new AbsolutePath(NewPath.GetFullPath(path, workingDir!));
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
{
// For invalid paths (e.g., wildcards) fall back to a simple combination,
// preserving the non-throwing behavior of File.Exists/Directory.Exists.
return new AbsolutePath(path, new AbsolutePath(workingDir!));
}
}
/// <summary>
/// Get the hex hash string for the string
/// </summary>
internal static string GetHexHash(string stringToHash)
{
return stringToHash.GetHashCode().ToString("X", CultureInfo.InvariantCulture);
}
/// <summary>
/// Get the hash for the assemblyPaths
/// </summary>
internal static int GetPathsHash(IEnumerable<string> assemblyPaths)
{
StringBuilder builder = new StringBuilder();
foreach (string path in assemblyPaths)
{
if (path != null)
{
string directoryPath = path.Trim();
if (directoryPath.Length > 0)
{
DateTime lastModifiedTime;
if (NativeMethods.GetLastWriteDirectoryUtcTime(directoryPath, out lastModifiedTime))
{
builder.Append(lastModifiedTime.Ticks);
builder.Append('|');
builder.Append(directoryPath.ToUpperInvariant());
builder.Append('|');
}
}
}
}
return builder.ToString().GetHashCode();
}
/// <summary>
/// Returns whether MSBuild can write to the given directory. Throws for PathTooLongExceptions
/// but not other exceptions.
/// </summary>
internal static bool CanWriteToDirectory(string directory)
{
try
{
string testFilePath = Path.Combine(directory, $"MSBuild_{Guid.NewGuid():N}_testFile.txt");
FileInfo file = new(testFilePath);
file.Directory!.Create(); // If the directory already exists, this method does nothing.
File.WriteAllText(testFilePath, $"MSBuild process {EnvironmentUtilities.CurrentProcessId} successfully wrote to file.");
File.Delete(testFilePath);
return true;
}
catch (PathTooLongException)
{
throw new ArgumentException(SR.FormatDebugPathTooLong(directory));
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Ensures the path does not have a leading or trailing slash after removing the first 'start' characters.
/// </summary>
internal static string EnsureNoLeadingOrTrailingSlash(string path, int start)
{
int stop = path.Length;
while (start < stop && IsSlash(path[start]))
{
start++;
}
while (start < stop && IsSlash(path[stop - 1]))
{
stop--;
}
return FixFilePath(path.Substring(start, stop - start));
}
/// <summary>
/// Ensures the path does not have a leading slash after removing the first 'start' characters but does end in a slash.
/// </summary>
internal static string EnsureTrailingNoLeadingSlash(string path, int start)
{
int stop = path.Length;
while (start < stop && IsSlash(path[start]))
{
start++;
}
return FixFilePath(start < stop && IsSlash(path[stop - 1]) ?
path.Substring(start) :
#if NET
string.Concat(path.AsSpan(start), new(in Path.DirectorySeparatorChar)));
#else
path.Substring(start) + Path.DirectorySeparatorChar);
#endif
}
/// <summary>
/// Ensures the path is enclosed within single quotes.
/// </summary>
/// <param name="path">The path to check.</param>
/// <returns>The path enclosed by quotes.</returns>
internal static string EnsureSingleQuotes(string path)
{
return EnsureQuotes(path);
}
/// <summary>
/// Ensures the path is enclosed within double quotes.
/// </summary>
/// <param name="path">The path to check.</param>
/// <returns>The path enclosed by quotes.</returns>
internal static string EnsureDoubleQuotes(string path)
{
return EnsureQuotes(path, isSingleQuote: false);
}
/// <summary>
/// Ensures the path is enclosed within quotes.
/// </summary>
/// <param name="path">The path to check.</param>
/// <param name="isSingleQuote">Indicates if single or double quotes should be used</param>
/// <returns>The path enclosed by quotes.</returns>
internal static string EnsureQuotes(string path, bool isSingleQuote = true)
{
path = FixFilePath(path);
const char singleQuote = '\'';
const char doubleQuote = '\"';
var targetQuote = isSingleQuote ? singleQuote : doubleQuote;
var convertQuote = isSingleQuote ? doubleQuote : singleQuote;
if (!string.IsNullOrEmpty(path))
{
// Special case: convert the quotes.
if (path.Length > 1 && path[0] == convertQuote && path[path.Length - 1] == convertQuote)
{
#if NET
path = $"{targetQuote}{path.AsSpan(1, path.Length - 2)}{targetQuote}";
#else
path = $"{targetQuote}{path.Substring(1, path.Length - 2)}{targetQuote}";
#endif
}
// Enclose the path in a set of the 'target' quote unless the string is already quoted with the 'target' quotes.
else if (path.Length == 1 || path[0] != targetQuote || path[path.Length - 1] != targetQuote)
{
path = $"{targetQuote}{path}{targetQuote}";
}
}
return path;
}
/// <summary>
/// Trims the string and removes any double quotes around it.
/// </summary>
[return: NotNullIfNotNull(nameof(path))]
internal static string? TrimAndStripAnyQuotes(string? path)
{
if (path is null)
{
return path;
}
// Trim returns the same string if trimming isn't needed
path = path.Trim();
path = path.Trim(['"']);
return path;
}
/// <summary>
/// Get the directory name of a rooted full path
/// </summary>
/// <param name="fullPath"></param>
/// <returns></returns>
internal static string? GetDirectoryNameOfFullPath(string fullPath)
{
if (fullPath != null)
{
int i = fullPath.Length;
while (i > 0 && fullPath[--i] != Path.DirectorySeparatorChar && fullPath[i] != Path.AltDirectorySeparatorChar)
{
;
}
return FixFilePath(fullPath.Substring(0, i));
}
return null;
}
internal static string TruncatePathToTrailingSegments(string path, int trailingSegmentsToKeep)
{
Assumed.NotNullOrEmpty(path);
Assumed.PositiveOrZero(trailingSegmentsToKeep, "trailing segments must be positive");
var segments = path.Split(Slashes, StringSplitOptions.RemoveEmptyEntries);
var headingSegmentsToRemove = Math.Max(0, segments.Length - trailingSegmentsToKeep);
return string.Join(DirectorySeparatorString, segments.Skip(headingSegmentsToRemove));
}
internal static bool ContainsRelativePathSegments(string path)
{
for (int i = 0; i < path.Length; i++)
{
if (i + 1 < path.Length && path[i] == '.' && path[i + 1] == '.')
{
if (RelativePathBoundsAreValid(path, i, i + 1))
{
return true;
}
else
{
i += 2;
continue;
}
}
if (path[i] == '.' && RelativePathBoundsAreValid(path, i, i))
{
return true;
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool RelativePathBoundsAreValid(string path, int leftIndex, int rightIndex)
{
var leftBound = leftIndex - 1 >= 0
? path[leftIndex - 1]
: (char?)null;
var rightBound = rightIndex + 1 < path.Length
? path[rightIndex + 1]
: (char?)null;
return IsValidRelativePathBound(leftBound) && IsValidRelativePathBound(rightBound);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsValidRelativePathBound(char? c)
{
return c == null || IsAnySlash(c.Value);
}
/// <summary>
/// Returns true if any path segment is exactly "..", without allocating.
/// </summary>
internal static bool ContainsParentTraversalSegment(ReadOnlySpan<char> path)
{
// Walk each segment; return true only for a segment that is exactly "..".
while (!path.IsEmpty)
{
int sep = path.IndexOfAny(Slashes);
ReadOnlySpan<char> segment = sep < 0 ? path : path.Slice(0, sep);
if (segment.SequenceEqual("..".AsSpan()))
{
return true;
}
path = sep < 0 ? default : path.Slice(sep + 1);
}
return false;
}
/// <summary>
/// Gets the canonicalized full path of the provided path.
/// Guidance for use: call this on all paths accepted through public entry
/// points that need normalization. After that point, only verify the path
/// is rooted, using ErrorUtilities.VerifyThrowPathRooted.
/// ASSUMES INPUT IS ALREADY UNESCAPED.
/// </summary>
internal static string NormalizePath(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
string fullPath = NewPath.GetFullPath(path);
return FixFilePath(fullPath);
}
internal static string NormalizePath(string directory, string file)
{
return NormalizePath(Path.Combine(directory, file));
}
internal static string NormalizePath(params string[] paths)
{
return NormalizePath(Path.Combine(paths));
}
/// <summary>
/// Normalizes all path separators (both forward and back slashes) to forward slashes.
/// This is platform-independent, unlike FrameworkFileUtilities.FixFilePath which only normalizes on non-Windows platforms.
/// Use this when you need consistent path comparison regardless of which separator style is used.
/// </summary>
/// <param name="path">The path to normalize</param>
/// <returns>The path with all backslashes replaced by forward slashes, or the original path if null/empty</returns>
internal static string NormalizePathSeparatorsToForwardSlash(string path)
{
return string.IsNullOrEmpty(path) ? path : path.Replace('\\', '/');
}
/// <summary>
/// If on Unix, convert backslashes to slashes for strings that resemble paths.
/// The heuristic is if something resembles paths (contains slashes) check if the
/// first segment exists and is a directory.
/// Use a native shared method to massage file path. If the file is adjusted,
/// that qualifies is as a path.
///
/// @baseDirectory is just passed to LooksLikeUnixFilePath, to help with the check
/// </summary>
internal static string MaybeAdjustFilePath(string value, string baseDirectory = "")
{
var comparisonType = StringComparison.Ordinal;
// Don't bother with arrays or properties or network paths, or those that
// have no slashes.
if (NativeMethods.IsWindows || string.IsNullOrEmpty(value)
|| value.StartsWith("$(", comparisonType) || value.StartsWith("@(", comparisonType)
|| value.StartsWith("\\\\", comparisonType))
{
return value;
}
// For Unix-like systems, we may want to convert backslashes to slashes
Span<char> newValue = ConvertToUnixSlashes(value.ToCharArray());
// Find the part of the name we want to check, that is remove quotes, if present
bool shouldAdjust = newValue.IndexOf('/') != -1 && LooksLikeUnixFilePath(RemoveQuotes(newValue), baseDirectory);
return shouldAdjust ? newValue.ToString() : value;
}
/// <summary>
/// If on Unix, convert backslashes to slashes for strings that resemble paths.
/// This overload takes and returns ReadOnlyMemory of characters.
/// </summary>
internal static ReadOnlyMemory<char> MaybeAdjustFilePath(ReadOnlyMemory<char> value, string baseDirectory = "")
{
if (NativeMethods.IsWindows || value.IsEmpty)
{
return value;
}
// Don't bother with arrays or properties or network paths.
if (value.Length >= 2)
{
var span = value.Span;
// The condition is equivalent to span.StartsWith("$(") || span.StartsWith("@(") || span.StartsWith("\\\\")
if ((span[1] == '(' && (span[0] == '$' || span[0] == '@')) ||
(span[1] == '\\' && span[0] == '\\'))
{
return value;
}
}
// For Unix-like systems, we may want to convert backslashes to slashes
Span<char> newValue = ConvertToUnixSlashes(value.ToArray());
// Find the part of the name we want to check, that is remove quotes, if present
bool shouldAdjust = newValue.IndexOf('/') != -1 && LooksLikeUnixFilePath(RemoveQuotes(newValue), baseDirectory);
return shouldAdjust ? newValue.ToString().AsMemory() : value;
}
private static Span<char> ConvertToUnixSlashes(Span<char> path)
{
return path.IndexOf('\\') == -1 ? path : CollapseSlashes(path);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Span<char> CollapseSlashes(Span<char> str)
{
int sliceLength = 0;
// Performs Regex.Replace(str, @"[\\/]+", "/")
for (int i = 0; i < str.Length; i++)
{
bool isCurSlash = IsAnySlash(str[i]);
bool isPrevSlash = i > 0 && IsAnySlash(str[i - 1]);
if (!isCurSlash || !isPrevSlash)
{
str[sliceLength] = str[i] == '\\' ? '/' : str[i];
sliceLength++;
}
}
return str.Slice(0, sliceLength);
}
private static Span<char> RemoveQuotes(Span<char> path)
{
int endId = path.Length - 1;
char singleQuote = '\'';
char doubleQuote = '\"';
bool hasQuotes = path.Length > 2
&& ((path[0] == singleQuote && path[endId] == singleQuote)
|| (path[0] == doubleQuote && path[endId] == doubleQuote));
return hasQuotes ? path.Slice(1, endId - 1) : path;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsAnySlash(char c) => c == '/' || c == '\\';
/// <summary>
/// If on Unix, check if the string looks like a file path.
/// The heuristic is if something resembles paths (contains slashes) check if the
/// first segment exists and is a directory.
///
/// If @baseDirectory is not null, then look for the first segment exists under
/// that
/// </summary>
internal static bool LooksLikeUnixFilePath(string value, string baseDirectory = "")
=> LooksLikeUnixFilePath(value.AsSpan(), baseDirectory);
internal static bool LooksLikeUnixFilePath(ReadOnlySpan<char> value, string baseDirectory = "")
{
if (NativeMethods.IsWindows)
{
return false;
}
// In MT mode the process CWD should not be used when resolving the first relative path segment. Use the
// thread-local working directory so the directory existence heuristic runs against the correct project directory.
if (string.IsNullOrEmpty(baseDirectory))
{
baseDirectory = CurrentThreadWorkingDirectory ?? "";
}
// The first slash will either be at the beginning of the string or after the first directory name
int directoryLength = value.Slice(1).IndexOf('/') + 1;
bool shouldCheckDirectory = directoryLength != 0;
// Check for actual files or directories under / that get missed by the above logic
bool shouldCheckFileOrDirectory = !shouldCheckDirectory && value.Length > 0 && value[0] == '/';
ReadOnlySpan<char> directory = value.Slice(0, directoryLength);
return (shouldCheckDirectory && DefaultFileSystem.DirectoryExists(Path.Combine(baseDirectory, directory.ToString())))
|| (shouldCheckFileOrDirectory && DefaultFileSystem.FileOrDirectoryExists(value.ToString()));
}
/// <summary>
/// Extracts the directory from the given file-spec.
/// </summary>
/// <param name="fileSpec">The filespec.</param>
/// <returns>directory path</returns>
internal static string GetDirectory(string fileSpec)
{
string? directory = Path.GetDirectoryName(FixFilePath(fileSpec));
// if file-spec is a root directory e.g. c:, c:\, \, \\server\share
// NOTE: Path.GetDirectoryName also treats invalid UNC file-specs as root directories e.g. \\, \\server
if (directory == null)
{
// just use the file-spec as-is
directory = fileSpec;
}
else if ((directory.Length > 0) && !EndsWithSlash(directory))
{
// restore trailing slash if Path.GetDirectoryName has removed it (this happens with non-root directories)
directory += Path.DirectorySeparatorChar;
}
return directory;
}
/// <summary>
/// Deletes all subdirectories within the specified directory without throwing exceptions.
/// This method enumerates all subdirectories in the given directory and attempts to delete
/// each one recursively. If any IO-related exceptions occur during enumeration or deletion,
/// they are silently ignored.
/// </summary>
/// <param name="directory">The directory whose subdirectories should be deleted.</param>
/// <remarks>
/// This method is useful for cleanup operations where partial failure is acceptable.
/// It will not delete the root directory itself, only its subdirectories.
/// IO exceptions during directory enumeration or deletion are caught and ignored.
/// </remarks>
internal static void DeleteSubdirectoriesNoThrow(string directory)
{
try
{
foreach (string dir in FileSystems.Default.EnumerateDirectories(directory))
{
DeleteDirectoryNoThrow(dir, recursive: true, retryCount: 1);
}
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
// If we can't enumerate the directories, ignore. Other cases should be handled by DeleteDirectoryNoThrow.
}
}
/// <summary>
/// Determines whether the given assembly file name has one of the listed extensions.
/// </summary>
/// <param name="fileName">The name of the file</param>
/// <param name="allowedExtensions">Array of extensions to consider.</param>
/// <returns></returns>
internal static bool HasExtension(string fileName, string[] allowedExtensions)
{
Debug.Assert(allowedExtensions?.Length > 0);
// Easiest way to invoke invalid path chars
// check, which callers are relying on.
if (allowedExtensions != null && Path.HasExtension(fileName))
{
foreach (string extension in allowedExtensions)
{
Debug.Assert(!String.IsNullOrEmpty(extension) && extension[0] == '.');
if (fileName.EndsWith(extension, PathComparison))
{
return true;
}
}
}
return false;
}
// ISO 8601 Universal time with sortable format
internal const string FileTimeFormat = "yyyy'-'MM'-'dd HH':'mm':'ss'.'fffffff";
/// <summary>
/// Determines the full path for the given file-spec.
/// ASSUMES INPUT IS STILL ESCAPED
/// </summary>
/// <param name="fileSpec">The file spec to get the full path of.</param>
/// <param name="currentDirectory"></param>
/// <param name="escape">Whether to escape the path after getting the full path.</param>
/// <returns>Full path to the file, escaped if not specified otherwise.</returns>
internal static string GetFullPath(string fileSpec, string currentDirectory, bool escape = true)
{
// Sending data out of the engine into the filesystem, so time to unescape.
fileSpec = FixFilePath(EscapingUtilities.UnescapeAll(fileSpec));
string fullPath = NormalizePath(Path.Combine(currentDirectory, fileSpec));
// In some cases we might want to NOT escape in order to preserve symbols like @, %, $ etc.
if (escape)
{
// Data coming back from the filesystem into the engine, so time to escape it back.
fullPath = EscapingUtilities.Escape(fullPath);
}
if (NativeMethods.IsWindows && !EndsWithSlash(fullPath))
{
if (FileUtilitiesRegex.IsDrivePattern(fileSpec) ||
FileUtilitiesRegex.IsUncPattern(fullPath))
{
// append trailing slash if Path.GetFullPath failed to (this happens with drive-specs and UNC shares)
fullPath += Path.DirectorySeparatorChar;
}
}
return fullPath;
}
/// <summary>
/// A variation of Path.GetFullPath that will return the input value
/// instead of throwing any IO exception.
/// Useful to get a better path for an error message, without the risk of throwing
/// if the error message was itself caused by the path being invalid!
/// </summary>
internal static string GetFullPathNoThrow(string path)
{
try
{
path = NormalizePath(path);
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
}
return path;
}
/// <summary>
/// Compare if two paths, relative to the given currentDirectory are equal.
/// Does not throw IO exceptions. See <see cref="GetFullPathNoThrow(string)"/>
/// </summary>
/// <param name="first"></param>
/// <param name="second"></param>
/// <param name="currentDirectory"></param>
/// <param name="alwaysIgnoreCase"></param>
/// <returns></returns>
internal static bool ComparePathsNoThrow(string first, string second, string currentDirectory, bool alwaysIgnoreCase = false)
{
StringComparison pathComparison = alwaysIgnoreCase ? StringComparison.OrdinalIgnoreCase : PathComparison;
// perf: try comparing the bare strings first
if (string.Equals(first, second, pathComparison))
{
return true;
}
var firstFullPath = NormalizePathForComparisonNoThrow(first, currentDirectory);
var secondFullPath = NormalizePathForComparisonNoThrow(second, currentDirectory);
return string.Equals(firstFullPath, secondFullPath, pathComparison);
}
/// <summary>
/// Normalizes a path for path comparison
/// Does not throw IO exceptions. See <see cref="GetFullPathNoThrow(string)"/>
///
/// </summary>
internal static string NormalizePathForComparisonNoThrow(string path, string currentDirectory)
{
// file is invalid, return early to avoid triggering an exception
if (PathIsInvalid(path))
{
return path;
}
var normalizedPath = NormalizeForPathComparison(path);
var fullPath = GetFullPathNoThrow(Path.Combine(currentDirectory, normalizedPath));
return fullPath;
}
internal static bool PathIsInvalid(string path)
{
// Path.GetFileName does not react well to malformed filenames.
// For example, Path.GetFileName("a/b/foo:bar") returns bar instead of foo:bar
// It also throws exceptions on illegal path characters
#if NET
if (!path.AsSpan().ContainsAny(InvalidPathChars))
{
int lastDirectorySeparator = path.LastIndexOfAny(Slashes);
return path.AsSpan(lastDirectorySeparator >= 0 ? lastDirectorySeparator + 1 : 0).ContainsAny(InvalidFileNameChars);
}
#else
if (path.IndexOfAny(InvalidPathChars) < 0)
{
int lastDirectorySeparator = path.LastIndexOfAny(Slashes);
return path.IndexOfAny(InvalidFileNameChars, lastDirectorySeparator >= 0 ? lastDirectorySeparator + 1 : 0) >= 0;
}
#endif
return true;
}
/// <summary>
/// A variation on File.Delete that will throw ExceptionHandling.NotExpectedException exceptions
/// </summary>
internal static void DeleteNoThrow(string path)
{
try
{
File.Delete(FixFilePath(path));
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
}
}
/// <summary>
/// A variation on Directory.Delete that will throw ExceptionHandling.NotExpectedException exceptions
/// </summary>
[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")]
internal static void DeleteDirectoryNoThrow(string path, bool recursive, int retryCount = 0, int retryTimeOut = 0)
{
// Try parse will set the out parameter to 0 if the string passed in is null, or is outside the range of an int.
if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDDIRECTORYDELETERETRYCOUNT"), out retryCount))
{
retryCount = 0;
}
if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDDIRECTORYDELETRETRYTIMEOUT"), out retryTimeOut))
{
retryTimeOut = 0;
}
retryCount = retryCount < 1 ? 2 : retryCount;
retryTimeOut = retryTimeOut < 1 ? 500 : retryTimeOut;
path = FixFilePath(path);
for (int i = 0; i < retryCount; i++)
{
try
{
if (DefaultFileSystem.DirectoryExists(path))
{
Directory.Delete(path, recursive);
break;
}
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
}
if (i + 1 < retryCount) // should not wait for the final iteration since we not gonna check anyway
{
Thread.Sleep(retryTimeOut);
}
}
}
/// <summary>
/// Deletes a directory, ensuring that Directory.Delete does not get a path ending in a slash.
/// </summary>
/// <remarks>
/// This is a workaround for https://github.com/dotnet/corefx/issues/3780, which clashed with a common
/// pattern in our tests.
/// </remarks>
internal static void DeleteWithoutTrailingBackslash(string path, bool recursive = false)
{
// Some tests (such as FileMatcher and Evaluation tests) were failing with an UnauthorizedAccessException or directory not empty.
// This retry logic works around that issue.
const int NUM_TRIES = 3;
for (int i = 0; i < NUM_TRIES; i++)
{
try
{
Directory.Delete(EnsureNoTrailingSlash(path), recursive);
// If we got here, the directory was successfully deleted
return;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
if (i == NUM_TRIES - 1)
{
// var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
// string fileString = string.Join(Environment.NewLine, files);
// string message = $"Unable to delete directory '{path}'. Contents:" + Environment.NewLine + fileString;
// throw new IOException(message, ex);
throw;
}
}
Thread.Sleep(10);
}
}
/// <summary>
/// Gets a file info object for the specified file path. If the file path
/// is invalid, or is a directory, or cannot be accessed, or does not exist,
/// it returns null rather than throwing or returning a FileInfo around a non-existent file.
/// This allows it to be called where File.Exists() (which never throws, and returns false
/// for directories) was called - but with the advantage that a FileInfo object is returned
/// that can be queried (e.g., for LastWriteTime) without hitting the disk again.
/// </summary>
/// <param name="filePath"></param>
/// <returns>FileInfo around path if it is an existing /file/, else null</returns>
internal static FileInfo? GetFileInfoNoThrow(string filePath)
{
filePath = AttemptToShortenPath(filePath);
FileInfo fileInfo;
try
{
fileInfo = new FileInfo(filePath);
}
catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
{
// Invalid or inaccessible path: treat as if nonexistent file, just as File.Exists does
return null;
}
if (fileInfo.Exists)
{
// It's an existing file
return fileInfo;
}
else
{
// Nonexistent, or existing but a directory, just as File.Exists behaves
return null;
}
}
/// <summary>
/// Returns if the directory exists
/// </summary>
/// <param name="fullPath">Full path to the directory in the filesystem</param>
/// <param name="fileSystem">The file system</param>
/// <returns></returns>
internal static bool DirectoryExistsNoThrow(string fullPath, IFileSystem? fileSystem = null)
{
fullPath = AttemptToShortenPath(fullPath);
try
{
fileSystem ??= DefaultFileSystem;
return Traits.Instance.CacheFileExistence
? FileExistenceCache.GetOrAdd(fullPath, fullPath => fileSystem.DirectoryExists(fullPath))
: fileSystem.DirectoryExists(fullPath);
}
catch
{
return false;
}
}
/// <summary>
/// Returns if the directory exists
/// </summary>
/// <param name="fullPath">Full path to the file in the filesystem</param>
/// <param name="fileSystem">The file system</param>
/// <returns></returns>
internal static bool FileExistsNoThrow(string fullPath, IFileSystem? fileSystem = null)
{
fullPath = AttemptToShortenPath(fullPath);
try
{
fileSystem ??= DefaultFileSystem;
return Traits.Instance.CacheFileExistence
? FileExistenceCache.GetOrAdd(fullPath, fullPath => fileSystem.FileExists(fullPath))
: fileSystem.FileExists(fullPath);
}
catch
{
return false;
}
}
/// <summary>
/// If there is a directory or file at the specified path, returns true.
/// Otherwise, returns false.
/// Does not throw IO exceptions, to match Directory.Exists and File.Exists.
/// Unlike calling each of those in turn it only accesses the disk once, which is faster.
/// </summary>
internal static bool FileOrDirectoryExistsNoThrow(string fullPath, IFileSystem? fileSystem = null)
{
fullPath = AttemptToShortenPath(fullPath);
try
{
fileSystem ??= DefaultFileSystem;
return Traits.Instance.CacheFileExistence
? FileExistenceCache.GetOrAdd(fullPath, fullPath => fileSystem.FileOrDirectoryExists(fullPath))
: fileSystem.FileOrDirectoryExists(fullPath);
}
catch
{
return false;
}
}
/// <summary>
/// This method returns true if the specified filename is a solution file (.sln) or
/// solution filter file (.slnf); otherwise, it returns false.
/// </summary>
/// <remarks>
/// Solution filters are included because they are a thin veneer over solutions, just
/// with a more limited set of projects to build, and should be treated the same way.
/// </remarks>
internal static bool IsSolutionFilename(string filename)
{
return HasExtension(filename, ".sln") ||
HasExtension(filename, ".slnf") ||
HasExtension(filename, ".slnx");
}
internal static bool IsSolutionFilterFilename(string filename)
{
return HasExtension(filename, ".slnf");
}
internal static bool IsSolutionXFilename(string filename)
{
return HasExtension(filename, ".slnx");
}
/// <summary>
/// Returns true if the specified filename is a VC++ project file, otherwise returns false
/// </summary>
internal static bool IsVCProjFilename(string filename)
{
return HasExtension(filename, ".vcproj");
}
internal static bool IsDspFilename(string filename)
{
return HasExtension(filename, ".dsp");
}
/// <summary>
/// Returns true if the specified filename is a metaproject file (.metaproj), otherwise false.
/// </summary>
internal static bool IsMetaprojectFilename(string? filename)
{
return HasExtension(filename, ".metaproj");
}
internal static bool IsBinaryLogFilename(string filename)
{
return HasExtension(filename, ".binlog");
}
private static bool HasExtension(string? filename, string extension)
{
if (String.IsNullOrEmpty(filename))
{
return false;
}
return filename!.EndsWith(extension, PathComparison);
}
/// <summary>
/// Given the absolute location of a file, and a disc location, returns relative file path to that disk location.
/// Throws UriFormatException.
/// </summary>
/// <param name="basePath">
/// The base path we want to be relative to. Must be absolute.
/// Should <i>not</i> include a filename as the last segment will be interpreted as a directory.
/// </param>
/// <param name="path">
/// 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.
/// If the path cannot be made relative to the base path (for example, it is on another drive), it is returned verbatim.
/// If the basePath is an empty string, returns the path.
/// </param>
/// <returns>relative path (can be the full path)</returns>
internal static string MakeRelative(string basePath, string path)
{
ArgumentNullException.ThrowIfNull(basePath);
ArgumentException.ThrowIfNullOrEmpty(path);
string fullBase = NewPath.GetFullPath(basePath);
string fullPath = NewPath.GetFullPath(path);
string[] splitBase = fullBase.Split(MSBuildConstants.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
string[] splitPath = fullPath.Split(MSBuildConstants.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
Assumed.Positive(splitPath.Length, "Cannot call MakeRelative on a path of only slashes.");
// On a mac, the path could start with any number of slashes and still be valid. We have to check them all.
int indexOfFirstNonSlashChar = 0;
while (path[indexOfFirstNonSlashChar] == Path.DirectorySeparatorChar)
{
indexOfFirstNonSlashChar++;
}
if (path.IndexOf(splitPath[0], PathComparison) != indexOfFirstNonSlashChar)
{
// path was already relative so just return it
return FixFilePath(path);
}
int index = 0;
while (index < splitBase.Length && index < splitPath.Length && splitBase[index].Equals(splitPath[index], PathComparison))
{
index++;
}
if (index == splitBase.Length && index == splitPath.Length)
{
return ".";
}
// If the paths have no component in common, the only valid relative path is the full path.
if (index == 0)
{
return fullPath;
}
StringBuilder sb = StringBuilderCache.Acquire();
for (int i = index; i < splitBase.Length; i++)
{
sb.Append("..").Append(Path.DirectorySeparatorChar);
}
for (int i = index; i < splitPath.Length; i++)
{
sb.Append(splitPath[i]).Append(Path.DirectorySeparatorChar);
}
if (fullPath[fullPath.Length - 1] != Path.DirectorySeparatorChar)
{
sb.Length--;
}
return StringBuilderCache.GetStringAndRelease(sb);
}
/// <summary>
/// Normalizes the path if and only if it is longer than max path,
/// or would be if rooted by the current directory.
/// This may make it shorter by removing ".."'s.
/// </summary>
internal static string AttemptToShortenPath(string path)
{
if (IsPathTooLong(path) || IsPathTooLongIfRooted(path))
{
// Attempt to make it shorter -- perhaps there are some \..\ elements
path = GetFullPathNoThrow(path);
}
return FixFilePath(path);
}
private static bool IsPathTooLongIfRooted(string path)
{
bool hasMaxPath = NativeMethods.HasMaxPath;
int maxPath = NativeMethods.MaxPath;
// >= not > because MAX_PATH assumes a trailing null
return hasMaxPath && !IsRootedNoThrow(path) && Environment.CurrentDirectory.Length + path.Length + 1 /* slash */ >= maxPath;
}
/// <summary>
/// A variation of Path.IsRooted that not throw any IO exception.
/// </summary>
private static bool IsRootedNoThrow(string path)
{
try
{
return Path.IsPathRooted(FixFilePath(path));
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
return false;
}
}
/// <summary>
/// Get the folder N levels above the given. Will stop and return current path when rooted.
/// </summary>
/// <param name="path">Path to get the folder above.</param>
/// <param name="count">Number of levels up to walk.</param>
/// <returns>Full path to the folder N levels above the path.</returns>
internal static string GetFolderAbove(string path, int count = 1)
{
if (count < 1)
{
return path;
}
var parent = Directory.GetParent(path);
while (count > 1 && parent?.Parent != null)
{
parent = parent.Parent;
count--;
}
return parent?.FullName ?? path;
}
/// <summary>
/// Combine multiple paths. Should only be used when compiling against .NET 2.0.
/// <remarks>
/// Only use in .NET 2.0. Otherwise, use System.IO.Path.Combine(...)
/// </remarks>
/// </summary>
/// <param name="root">Root path.</param>
/// <param name="paths">Paths to concatenate.</param>
/// <returns>Combined path.</returns>
internal static string CombinePaths(string root, params string[] paths)
{
ArgumentNullException.ThrowIfNull(root);
ArgumentNullException.ThrowIfNull(paths);
return paths.Aggregate(root, Path.Combine);
}
internal static string TrimTrailingSlashes(this string s)
{
return s.TrimEnd(Slashes);
}
/// <summary>
/// Replace all backward slashes to forward slashes
/// </summary>
internal static string ToSlash(this string s)
{
return s.Replace('\\', '/');
}
internal static string ToBackslash(this string s)
{
return s.Replace('/', '\\');
}
/// <summary>
/// Ensure all slashes are the current platform's slash
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
internal static string ToPlatformSlash(this string s)
{
var separator = Path.DirectorySeparatorChar;
return s.Replace(separator == '/' ? '\\' : '/', separator);
}
internal static string WithTrailingSlash(this string s)
{
return EnsureTrailingSlash(s);
}
internal static string NormalizeForPathComparison(this string s)
=> s.ToPlatformSlash().TrimTrailingSlashes();
// TODO: assumption on file system case sensitivity: https://github.com/dotnet/msbuild/issues/781
internal static bool PathsEqual(string path1, string path2)
{
if (path1 == null && path2 == null)
{
return true;
}
if (path1 == null || path2 == null)
{
return false;
}
var endA = path1.Length - 1;
var endB = path2.Length - 1;
// Trim trailing slashes
for (var i = endA; i >= 0; i--)
{
var c = path1[i];
if (c == '/' || c == '\\')
{
endA--;
}
else
{
break;
}
}
for (var i = endB; i >= 0; i--)
{
var c = path2[i];
if (c == '/' || c == '\\')
{
endB--;
}
else
{
break;
}
}
if (endA != endB)
{
// Lengths not the same
return false;
}
for (var i = 0; i <= endA; i++)
{
var charA = (uint)path1[i];
var charB = (uint)path2[i];
if ((charA | charB) > 0x7F)
{
// Non-ascii chars move to non fast path
return PathsEqualNonAscii(path1, path2, i, endA - i + 1);
}
// uppercase both chars - notice that we need just one compare per char
if ((uint)(charA - 'a') <= (uint)('z' - 'a'))
{
charA -= 0x20;
}
if ((uint)(charB - 'a') <= (uint)('z' - 'a'))
{
charB -= 0x20;
}
// Set path delimiters the same
if (charA == '\\')
{
charA = '/';
}
if (charB == '\\')
{
charB = '/';
}
if (charA != charB)
{
return false;
}
}
return true;
}
internal static StreamWriter OpenWrite(string path, bool append, Encoding? encoding = null)
{
const int DefaultFileStreamBufferSize = 4096;
FileMode mode = append ? FileMode.Append : FileMode.Create;
Stream fileStream = new FileStream(path, mode, FileAccess.Write, FileShare.Read, DefaultFileStreamBufferSize, FileOptions.SequentialScan);
if (encoding == null)
{
return new StreamWriter(fileStream);
}
else
{
return new StreamWriter(fileStream, encoding);
}
}
internal static StreamReader OpenRead(string path, Encoding? encoding = null, bool detectEncodingFromByteOrderMarks = true)
{
const int DefaultFileStreamBufferSize = 4096;
Stream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultFileStreamBufferSize, FileOptions.SequentialScan);
if (encoding == null)
{
return new StreamReader(fileStream);
}
else
{
return new StreamReader(fileStream, encoding, detectEncodingFromByteOrderMarks);
}
}
/// <summary>
/// Locate a file in either the directory specified or a location in the
/// directory structure above that directory.
/// </summary>
internal static string GetDirectoryNameOfFileAbove(string startingDirectory, string fileName, IFileSystem? fileSystem = null)
{
fileSystem ??= DefaultFileSystem;
// Canonicalize our starting location
string? lookInDirectory = NewPath.GetFullPath(startingDirectory);
do
{
// Construct the path that we will use to test against
string possibleFileDirectory = Path.Combine(lookInDirectory, fileName);
// If we successfully locate the file in the directory that we're
// looking in, simply return that location. Otherwise we'll
// keep moving up the tree.
if (fileSystem.FileExists(possibleFileDirectory))
{
// We've found the file, return the directory we found it in
return lookInDirectory;
}
else
{
// GetDirectoryName will return null when we reach the root
// terminating our search
lookInDirectory = Path.GetDirectoryName(lookInDirectory);
}
}
while (lookInDirectory != null);
// When we didn't find the location, then return an empty string
return string.Empty;
}
/// <summary>
/// Searches for a file based on the specified starting directory.
/// </summary>
/// <param name="file">The file to search for.</param>
/// <param name="startingDirectory">An optional directory to start the search in. The default location is the directory
/// of the file containing the property function.</param>
/// <param name="fileSystem">The filesystem</param>
/// <returns>The full path of the file if it is found, otherwise an empty string.</returns>
internal static string GetPathOfFileAbove(string file, string startingDirectory, IFileSystem? fileSystem = null)
{
// This method does not accept a path, only a file name
if (file.Any(i => i.Equals(Path.DirectorySeparatorChar) || i.Equals(Path.AltDirectorySeparatorChar)))
{
throw new ArgumentException(SR.FormatInvalidGetPathOfFileAboveParameter(file));
}
// Search for a directory that contains that file
string directoryName = GetDirectoryNameOfFileAbove(startingDirectory, file, fileSystem);
return String.IsNullOrEmpty(directoryName) ? String.Empty : NormalizePath(directoryName, file);
}
internal static void EnsureDirectoryExists(string directoryPath)
{
if (!string.IsNullOrEmpty(directoryPath) && !DefaultFileSystem.DirectoryExists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
}
// Method is simple set of function calls and may inline;
// we don't want it inlining into the tight loop that calls it as an exit case,
// so mark as non-inlining
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool PathsEqualNonAscii(string strA, string strB, int i, int length)
{
if (string.Compare(strA, i, strB, i, length, StringComparison.OrdinalIgnoreCase) == 0)
{
return true;
}
var slash1 = ToSlash(strA);
var slash2 = ToSlash(strB);
if (string.Compare(slash1, i, slash2, i, length, StringComparison.OrdinalIgnoreCase) == 0)
{
return true;
}
return false;
}
/// <summary>
/// Clears the file existence cache.
/// </summary>
internal static void ClearFileExistenceCache()
{
FileExistenceCache.Clear();
}
internal static void ReadFromStream(this Stream stream, byte[] content, int startIndex, int length)
{
stream.ReadExactly(content, startIndex, length);
}
}
}