| File: PathHelpers\AbsolutePath.cs | Web Access |
| Project: src\msbuild\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework) |
// 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; } }