// 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); } } } |