|
// 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 NETFRAMEWORK
using Microsoft.IO;
#else
using System.IO;
#endif
namespace Microsoft.Build.Framework
{
/// <summary>
/// Represents an absolute file system path.
/// </summary>
/// <remarks>
/// This struct wraps a string representing an absolute file system path.
/// Path equality comparisons are case-sensitive or case-insensitive depending on the operating system's
/// file system conventions (case-sensitive on Linux, case-insensitive on Windows and macOS).
/// Does not perform any normalization beyond validating the path is fully qualified.
/// A default instance (created via <c>default(AbsolutePath)</c>) has a null Value
/// and represents an issue in path handling. Two default instances are considered equal.
/// </remarks>
public readonly struct AbsolutePath : IEquatable<AbsolutePath>
{
/// <summary>
/// The string comparer to use for path comparisons, based on OS file system case sensitivity.
/// </summary>
private static readonly StringComparer s_pathComparer = NativeMethods.IsFileSystemCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
/// <summary>
/// The normalized string representation of this path.
/// </summary>
public string Value { get; }
/// <summary>
/// The original string used to create this path.
/// </summary>
public string OriginalValue { get; }
/// <summary>
/// Initializes a new instance of the <see cref="AbsolutePath"/> struct.
/// </summary>
/// <param name="path">The absolute path string.</param>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is null, empty, or not a rooted path.</exception>
public AbsolutePath(string path)
{
ValidatePath(path);
Value = path;
OriginalValue = path;
}
/// <summary>
/// Initializes a new instance of the <see cref="AbsolutePath"/> struct.
/// </summary>
/// <param name="path">The absolute path string.</param>
/// <param name="ignoreRootedCheck">If true, skips checking whether the path is rooted.</param>
/// <remarks>For internal and testing use, when we want to force bypassing the rooted check.</remarks>
internal AbsolutePath(string path, bool ignoreRootedCheck)
: this(path, path, ignoreRootedCheck)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AbsolutePath"/> struct.
/// </summary>
/// <param name="path">The absolute path string.</param>
/// <param name="original">The original string used to create this path.</param>
/// <param name="ignoreRootedCheck">If true, skips checking whether the path is rooted.</param>
internal AbsolutePath(string path, string original, bool ignoreRootedCheck)
{
if (!ignoreRootedCheck)
{
ValidatePath(path);
}
Value = path;
OriginalValue = original;
}
/// <summary>
/// Validates that the specified file system path is non-empty and rooted.
/// </summary>
/// <param name="path">The file system path to validate. Must not be null, empty, or a relative path.</param>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is null, empty, or not a rooted path.</exception>
private static void ValidatePath(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
// Path.IsPathFullyQualified is not available in .NET Standard 2.0
// in .NET Framework it's provided by package and in .NET it's built-in
#if NETFRAMEWORK || NET
if (!Path.IsPathFullyQualified(path))
{
throw new ArgumentException(SR.PathMustBeRooted, nameof(path));
}
#endif
}
/// <summary>
/// Initializes a new instance of the <see cref="AbsolutePath"/> struct by combining an absolute path with a relative path.
/// </summary>
/// <param name="path">The path to combine with the base path.</param>
/// <param name="basePath">The base path to combine with.</param>
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is null or empty.</exception>
public AbsolutePath(string path, AbsolutePath basePath)
{
ArgumentException.ThrowIfNullOrEmpty(path);
// This function should not throw when path has illegal characters.
// For .NET Framework, Microsoft.IO.Path.Combine should be used instead of System.IO.Path.Combine to achieve it.
// For .NET Core, System.IO.Path.Combine already does not throw in this case.
string combined = Path.Combine(basePath.Value, path);
// Path.IsPathFullyQualified is not available in .NET Standard 2.0
// in .NET Framework it's provided by package and in .NET it's built-in
// The netstandard2.0 build of Microsoft.Build.Framework exists only as a compatibility surface
#if NETFRAMEWORK || NET
combined = MakeFullyQualifiedRelativeToBasePath(combined, basePath.Value);
#endif
Value = combined;
OriginalValue = path;
}
#if NETFRAMEWORK || NET
/// <summary>
/// Anchors Windows rooted-but-not-fully-qualified paths (<c>"\foo"</c>, <c>"X:foo"</c>) to
/// <paramref name="basePath"/> so the result is independent of the current drive and per-drive cwd.
/// </summary>
/// <remarks>
/// Uses string operations instead of <c>Path.GetFullPath(path, basePath)</c> to preserve the
/// un-normalized form; <see cref="GetCanonicalForm"/> is the single normalization step.
/// </remarks>
private static string MakeFullyQualifiedRelativeToBasePath(string combined, string basePath)
{
if (!NativeMethods.IsWindows
|| string.IsNullOrEmpty(combined)
|| Path.IsPathFullyQualified(combined))
{
return combined;
}
char first = combined[0];
// Root-relative ("\foo"): re-root under basePath's root.
if (first == '\\' || first == '/')
{
string? root = Path.GetPathRoot(basePath);
return root is { Length: > 0 } baseRoot
? baseRoot.TrimEnd('\\', '/') + combined
: combined;
}
// Drive-relative ("X:foo"): drop the unrelated drive and anchor the remainder to basePath.
if (combined.Length >= 2 && combined[1] == ':')
{
return Path.Combine(basePath, combined.Substring(2));
}
return combined;
}
#endif
/// <summary>
/// Implicitly converts an AbsolutePath to a string.
/// </summary>
/// <param name="path">The path to convert.</param>
public static implicit operator string(AbsolutePath path) => path.Value;
/// <summary>
/// Returns the canonical form of this path, equivalent to calling <see cref="System.IO.Path.GetFullPath(string)"/>.
/// </summary>
/// <returns>
/// An <see cref="AbsolutePath"/> representing the canonical form of the path.
/// </returns>
/// <remarks>
/// <para>
/// The canonical form of a path is exactly what <see cref="System.IO.Path.GetFullPath(string)"/> would produce,
/// with the following properties:
/// <list type="bullet">
/// <item>All relative path segments ("." and "..") are resolved.</item>
/// <item>Directory separators are normalized to the platform convention (backslash on Windows).</item>
/// <item>Invalid path characters are rejected.</item>
/// </list>
/// </para>
/// <para>
/// Preserves the OriginalValue of the current instance.
/// </para>
/// </remarks>
internal AbsolutePath GetCanonicalForm()
{
return new AbsolutePath(System.IO.Path.GetFullPath(Value), OriginalValue, ignoreRootedCheck: true);
}
/// <summary>
/// Determines whether two <see cref="AbsolutePath"/> instances are equal.
/// </summary>
/// <param name="left">The first path to compare.</param>
/// <param name="right">The second path to compare.</param>
/// <returns><c>true</c> if the paths are equal; otherwise, <c>false</c>.</returns>
public static bool operator ==(AbsolutePath left, AbsolutePath right) => left.Equals(right);
/// <summary>
/// Determines whether two <see cref="AbsolutePath"/> instances are not equal.
/// </summary>
/// <param name="left">The first path to compare.</param>
/// <param name="right">The second path to compare.</param>
/// <returns><c>true</c> if the paths are not equal; otherwise, <c>false</c>.</returns>
public static bool operator !=(AbsolutePath left, AbsolutePath right) => !left.Equals(right);
/// <summary>
/// Determines whether the specified object is equal to the current <see cref="AbsolutePath"/>.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns><c>true</c> if the specified object is an <see cref="AbsolutePath"/> and is equal to the current instance; otherwise, <c>false</c>.</returns>
public override bool Equals(object? obj) => obj is AbsolutePath other && Equals(other);
/// <summary>
/// Determines whether the specified <see cref="AbsolutePath"/> is equal to the current instance.
/// </summary>
/// <param name="other">The <see cref="AbsolutePath"/> to compare with the current instance.</param>
/// <returns><c>true</c> if the paths are equal according to the operating system's file system case sensitivity rules; otherwise, <c>false</c>.</returns>
public bool Equals(AbsolutePath other) => s_pathComparer.Equals(Value, other.Value);
/// <summary>
/// Returns a hash code for this <see cref="AbsolutePath"/>.
/// </summary>
/// <returns>A hash code that is consistent with the equality comparison.</returns>
public override int GetHashCode() => Value is null ? 0 : s_pathComparer.GetHashCode(Value);
/// <summary>
/// Returns the string representation of this path.
/// </summary>
/// <returns>The path as a string.</returns>
public override string ToString() => Value;
}
}
|