File: PathDefaultClassifier.cs
Web Access
Project: TaskAnalyzer.csproj (Microsoft.Build.TaskAuthoring.Analyzer)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;

namespace Microsoft.Build.TaskAuthoring.Analyzer
{
    /// <summary>
    /// Classifies string default values for path-typed task properties. Shared by the analyzer (to decide
    /// whether a default is a relative path that must be moved into <c>Execute()</c>) and the code fixer (to
    /// decide whether a default can be reproduced directly in a property initializer).
    /// </summary>
    internal static class PathDefaultClassifier
    {
        /// <summary>
        /// Determines whether a default string value is an already fully-qualified (absolute) path. Only such
        /// values can be reproduced in a property initializer, because rooting a relative path requires
        /// <c>TaskEnvironment</c>, which the engine does not set until after the task is constructed.
        /// <para>
        /// This mirrors what <c>AbsolutePath</c> does at runtime (<see cref="System.IO.Path.IsPathFullyQualified(string)"/>,
        /// which is unavailable on netstandard2.0) by polyfilling the runtime's OS-specific
        /// <c>PathInternal.IsPartiallyQualified</c> logic. The check is OS-specific on purpose: on Unix only a
        /// path rooted at <c>'/'</c> is fully qualified, so a drive-style value like <c>"C:/x"</c> is treated as
        /// relative there — matching how <c>AbsolutePath</c> would root it.
        /// </para>
        /// </summary>
        public static bool IsFullyQualifiedPath(string value)
        {
            if (string.IsNullOrEmpty(value) || value.IndexOfAny(System.IO.Path.GetInvalidPathChars()) >= 0)
            {
                return false;
            }

            return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                ? IsFullyQualifiedWindows(value)
                : IsFullyQualifiedUnix(value);
        }

        /// <summary>
        /// Unix rule: a path is fully qualified only if it is rooted at <c>'/'</c>. Anything else (including
        /// Windows drive-relative or drive-absolute forms, which are meaningless on Unix) is relative.
        /// </summary>
        private static bool IsFullyQualifiedUnix(string value) => value[0] == '/';

        /// <summary>
        /// Windows rule, mirroring the negation of <c>PathInternal.IsPartiallyQualified</c>: a leading pair of
        /// separators (UNC/device, e.g. <c>\\server\share</c> or <c>\\?\</c>) or the drive-absolute
        /// <c>X:\</c>/<c>X:/</c> form is fully qualified. A single leading separator (<c>\foo</c>) is
        /// drive-relative and a drive-relative <c>X:foo</c> is not fully qualified.
        /// </summary>
        private static bool IsFullyQualifiedWindows(string value)
        {
            if (value.Length < 2)
            {
                return false;
            }

            if (IsDirectorySeparator(value[0]))
            {
                // Two leading separators, or "\?" (device-path prefix), are fully qualified.
                return value[1] == '?' || IsDirectorySeparator(value[1]);
            }

            // Drive-absolute "X:\" or "X:/" with a valid drive letter.
            return value.Length >= 3 &&
                value[1] == ':' &&
                IsDirectorySeparator(value[2]) &&
                IsValidDriveChar(value[0]);
        }

        private static bool IsDirectorySeparator(char c) => c == '\\' || c == '/';

        private static bool IsValidDriveChar(char c) =>
            (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');

        /// <summary>
        /// Determines whether a default string value is a plausible <em>relative</em> path: non-empty, free of
        /// characters that are never valid in a path, and not already fully qualified. These are the defaults
        /// that need rooting through <c>TaskEnvironment</c> inside <c>Execute()</c> (MSBuildTask0008).
        /// </summary>
        public static bool IsRelativePathDefault(string value)
        {
            return !string.IsNullOrEmpty(value) &&
                value.IndexOfAny(System.IO.Path.GetInvalidPathChars()) < 0 &&
                !IsFullyQualifiedPath(value);
        }
    }
}