Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 107 additions & 5 deletions src/Framework/FileUtilities.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NETFRAMEWORK && !TASKHOST
using Path = Microsoft.IO.Path;
#else
using System.IO;
#endif

namespace Microsoft.Build.Framework
{
Expand All @@ -14,10 +18,13 @@ namespace Microsoft.Build.Framework
/// </summary>
internal static class FrameworkFileUtilities
{
internal static readonly char[] Slashes = ['/', '\\'];
private const char UnixDirectorySeparator = '/';
private const char WindowsDirectorySeparator = '\\';

internal static readonly char[] Slashes = [UnixDirectorySeparator, WindowsDirectorySeparator];

/// <summary>
/// Indicates if the given character is a slash.
/// Indicates if the given character is a slash in current OS.
/// </summary>
/// <param name="c"></param>
/// <returns>true, if slash</returns>
Expand All @@ -38,9 +45,13 @@ internal static bool EndsWithSlash(string fileSpec)
: 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 == '\\' ? path : path.Replace('\\', '/');
return string.IsNullOrEmpty(path) || Path.DirectorySeparatorChar == WindowsDirectorySeparator ? path : path.Replace(WindowsDirectorySeparator, UnixDirectorySeparator);
}

/// <summary>
Expand All @@ -66,7 +77,7 @@ internal static string EnsureTrailingSlash(string fileSpec)
internal static string EnsureNoTrailingSlash(string path)
{
path = FixFilePath(path);
if (EndsWithSlash(path))
if (path.Length > 0 && IsSlash(path[path.Length - 1]))
{
path = path.Substring(0, path.Length - 1);
}
Expand All @@ -75,13 +86,54 @@ internal static string EnsureNoTrailingSlash(string path)
}

#if !TASKHOST
/// <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);
Expand All @@ -92,8 +144,24 @@ internal static AbsolutePath EnsureTrailingSlash(AbsolutePath path)
/// </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);
Expand All @@ -104,26 +172,60 @@ internal static AbsolutePath EnsureNoTrailingSlash(AbsolutePath 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(Path.GetFullPath(path.Value)),
original: path.OriginalValue,
ignoreRootedCheck: true);
}

/// <summary>
/// Resolves relative segments like "." and "..".
/// Resolves relative segments like "." and "..". Fixes directory separators on Windows like Path.GetFullPath does.
/// ASSUMES INPUT IS ALREADY UNESCAPED.
/// </summary>
internal static AbsolutePath RemoveRelativeSegments(AbsolutePath path)
{
if (string.IsNullOrEmpty(path.Value))
{
return path;
}

if (!MayHaveRelativeSegment(path.Value) && !HasUnixDirectorySeparatorOnWindows(path.Value))
{
return path;
}

return new AbsolutePath(Path.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);
Expand Down