File: src\Compilers\Core\Portable\InternalUtilities\FileNameUtilities.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Diagnostics.CodeAnalysis;
 
namespace Roslyn.Utilities
{
    /// <summary>
    /// Implements a few file name utilities that are needed by the compiler.
    /// In general the compiler is not supposed to understand the format of the paths.
    /// In rare cases it needs to check if a string is a valid file name or change the extension 
    /// (embedded resources, netmodules, output name).
    /// The APIs are intentionally limited to cover just these rare cases. Do not add more APIs.
    /// </summary>
    internal static class FileNameUtilities
    {
        internal const char DirectorySeparatorChar = '\\';
        internal const char AltDirectorySeparatorChar = '/';
        internal const char VolumeSeparatorChar = ':';
 
        /// <summary>
        /// Returns true if the string represents an unqualified file name. 
        /// The name may contain any characters but directory and volume separators.
        /// </summary>
        /// <param name="path">Path.</param>
        /// <returns>
        /// True if <paramref name="path"/> is a simple file name, false if it is null or includes a directory specification.
        /// </returns>
        internal static bool IsFileName([NotNullWhen(returnValue: true)] string? path)
        {
            return IndexOfFileName(path) == 0;
        }
 
        /// <summary>
        /// Returns the offset in <paramref name="path"/> where the dot that starts an extension is, or -1 if the path doesn't have an extension.
        /// </summary>
        /// <remarks>
        /// Returns 0 for path ".goo".
        /// Returns -1 for path "goo.".
        /// </remarks>
        private static int IndexOfExtension(string? path) =>
            path is null
                ? -1
                : IndexOfExtension(path.AsSpan());
 
        private static int IndexOfExtension(ReadOnlySpan<char> path)
        {
            int length = path.Length;
            int i = length;
 
            while (--i >= 0)
            {
                char c = path[i];
                if (c == '.')
                {
                    if (i != length - 1)
                    {
                        return i;
                    }
 
                    return -1;
                }
 
                if (c == DirectorySeparatorChar || c == AltDirectorySeparatorChar || c == VolumeSeparatorChar)
                {
                    break;
                }
            }
 
            return -1;
        }
 
        /// <summary>
        /// Returns an extension of the specified path string.
        /// </summary>
        /// <remarks>
        /// The same functionality as <see cref="System.IO.Path.GetExtension(string)"/> but doesn't throw an exception
        /// if there are invalid characters in the path.
        /// </remarks>
        [return: NotNullIfNotNull(parameterName: nameof(path))]
        internal static string? GetExtension(string? path)
        {
            if (path == null)
            {
                return null;
            }
 
            int index = IndexOfExtension(path);
            return (index >= 0) ? path.Substring(index) : string.Empty;
        }
 
        internal static ReadOnlyMemory<char> GetExtension(ReadOnlyMemory<char> path)
        {
            int index = IndexOfExtension(path.Span);
            return (index >= 0) ? path.Slice(index) : default;
        }
 
        /// <summary>
        /// Removes extension from path.
        /// </summary>
        /// <remarks>
        /// Returns "goo" for path "goo.".
        /// Returns "goo.." for path "goo...".
        /// </remarks>
        [return: NotNullIfNotNull(parameterName: nameof(path))]
        private static string? RemoveExtension(string? path)
        {
            if (path == null)
            {
                return null;
            }
 
            int index = IndexOfExtension(path);
            if (index >= 0)
            {
                return path.Substring(0, index);
            }
 
            // trim last ".", if present
            if (path.Length > 0 && path[path.Length - 1] == '.')
            {
                return path.Substring(0, path.Length - 1);
            }
 
            return path;
        }
 
        /// <summary>
        /// Returns path with the extension changed to <paramref name="extension"/>.
        /// </summary>
        /// <returns>
        /// Equivalent of <see cref="System.IO.Path.ChangeExtension(string, string)"/>
        /// 
        /// If <paramref name="path"/> is null, returns null. 
        /// If path does not end with an extension, the new extension is appended to the path.
        /// If extension is null, equivalent to <see cref="RemoveExtension"/>.
        /// </returns>
        [return: NotNullIfNotNull(parameterName: nameof(path))]
        internal static string? ChangeExtension(string? path, string? extension)
        {
            if (path == null)
            {
                return null;
            }
 
            var pathWithoutExtension = RemoveExtension(path);
            if (extension == null || path.Length == 0)
            {
                return pathWithoutExtension;
            }
 
            if (extension.Length == 0 || extension[0] != '.')
            {
                return pathWithoutExtension + "." + extension;
            }
 
            return pathWithoutExtension + extension;
        }
 
        /// <summary>
        /// Returns the position in given path where the file name starts.
        /// </summary>
        /// <returns>-1 if path is null.</returns>
        internal static int IndexOfFileName(string? path)
        {
            if (path == null)
            {
                return -1;
            }
 
            for (int i = path.Length - 1; i >= 0; i--)
            {
                char ch = path[i];
                if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar)
                {
                    return i + 1;
                }
            }
 
            return 0;
        }
 
        /// <summary>
        /// Get file name from path.
        /// </summary>
        /// <remarks>Unlike <see cref="System.IO.Path.GetFileName(string)"/> doesn't check for invalid path characters.</remarks>
        [return: NotNullIfNotNull(parameterName: nameof(path))]
        internal static string? GetFileName(string? path, bool includeExtension = true)
        {
            int fileNameStart = IndexOfFileName(path);
            var fileName = (fileNameStart <= 0) ? path : path!.Substring(fileNameStart);
            return includeExtension ? fileName : RemoveExtension(fileName);
        }
    }
}