// 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.Diagnostics; using System.Diagnostics.Contracts; using System.Text; namespace System.IO { // Provides methods for processing file system strings in a cross-platform manner. // Most of the methods don't do a complete parsing (such as examining a UNC hostname), // but they will handle most string operations. public static partial class Path { // Public static readonly variant of the separators. The Path implementation itself is using // internal const variant of the separators for better performance. public static readonly char DirectorySeparatorChar = PathInternal.DirectorySeparatorChar; public static readonly char AltDirectorySeparatorChar = PathInternal.AltDirectorySeparatorChar; public static readonly char VolumeSeparatorChar = PathInternal.VolumeSeparatorChar; public static readonly char PathSeparator = PathInternal.PathSeparator; // For generating random file names // 8 random bytes provides 12 chars in our encoding for the 8.3 name. const int KeyLength = 8; [Obsolete("Please use GetInvalidPathChars or GetInvalidFileNameChars instead.")] public static readonly char[] InvalidPathChars = GetInvalidPathChars(); // Changes the extension of a file path. The path parameter // specifies a file path, and the extension parameter // specifies a file extension (with a leading period, such as // ".exe" or ".cs"). // // The function returns a file path with the same root, directory, and base // name parts as path, but with the file extension changed to // the specified extension. If path is null, the function // returns null. If path does not contain a file extension, // the new file extension is appended to the path. If extension // is null, any existing extension is removed from path. public static string ChangeExtension(string path, string extension) { if (path != null) { PathInternal.CheckInvalidPathChars(path); string s = path; for (int i = path.Length - 1; i >= 0; i--) { char ch = path[i]; if (ch == '.') { s = path.Substring(0, i); break; } if (PathInternal.IsDirectoryOrVolumeSeparator(ch)) break; } if (extension != null && path.Length != 0) { s = (extension.Length == 0 || extension[0] != '.') ? s + "." + extension : s + extension; } return s; } return null; } // Returns the directory path of a file path. This method effectively // removes the last element of the given file path, i.e. it returns a // string consisting of all characters up to but not including the last // backslash ("\") in the file path. The returned value is null if the file // path is null or if the file path denotes a root (such as "\", "C:", or // "\\server\share"). public static string GetDirectoryName(string path) { if (path != null) { PathInternal.CheckInvalidPathChars(path); path = PathInternal.NormalizeDirectorySeparators(path); int root = PathInternal.GetRootLength(path); int i = path.Length; if (i > root) { while (i > root && !PathInternal.IsDirectorySeparator(path[--i])) ; return path.Substring(0, i); } } return null; } // Returns the extension of the given path. The returned value includes the // period (".") character of the extension except when you have a terminal period when you get string.Empty, such as ".exe" or // ".cpp". The returned value is null if the given path is // null or if the given path does not include an extension. [Pure] public static string GetExtension(string path) { if (path == null) return null; PathInternal.CheckInvalidPathChars(path); int length = path.Length; for (int i = length - 1; i >= 0; i--) { char ch = path[i]; if (ch == '.') { if (i != length - 1) return path.Substring(i, length - i); else return string.Empty; } if (PathInternal.IsDirectoryOrVolumeSeparator(ch)) break; } return string.Empty; } // Returns the name and extension parts of the given path. The resulting // string contains the characters of path that follow the last // separator in path. The resulting string is null if path is null. [Pure] public static string GetFileName(string path) { if (path == null) return null; int offset = PathInternal.FindFileNameIndex(path); int count = path.Length - offset; return path.Substring(offset, count); } [Pure] public static string GetFileNameWithoutExtension(string path) { if (path == null) return null; int length = path.Length; int offset = PathInternal.FindFileNameIndex(path); int end = path.LastIndexOf('.', length - 1, length - offset); return end == -1 ? path.Substring(offset) : // No extension was found path.Substring(offset, end - offset); } // Returns a cryptographically strong random 8.3 string that can be // used as either a folder name or a file name. public static unsafe string GetRandomFileName() { byte* pKey = stackalloc byte[KeyLength]; GetCryptoRandomBytes(pKey, KeyLength); const int RandomFileNameLength = 12; char* pRandomFileName = stackalloc char[RandomFileNameLength]; Populate83FileNameFromRandomBytes(pKey, KeyLength, pRandomFileName, RandomFileNameLength); return new string(pRandomFileName, 0, RandomFileNameLength); } // Tests if a path includes a file extension. The result is // true if the characters that follow the last directory // separator ('\\' or '/') or volume separator (':') in the path include // a period (".") other than a terminal period. The result is false otherwise. [Pure] public static bool HasExtension(string path) { if (path != null) { PathInternal.CheckInvalidPathChars(path); for (int i = path.Length - 1; i >= 0; i--) { char ch = path[i]; if (ch == '.') { return i != path.Length - 1; } if (PathInternal.IsDirectoryOrVolumeSeparator(ch)) break; } } return false; } public static string Combine(string path1, string path2) { if (path1 == null || path2 == null) throw new ArgumentNullException((path1 == null) ? nameof(path1): nameof(path2)); Contract.EndContractBlock(); PathInternal.CheckInvalidPathChars(path1); PathInternal.CheckInvalidPathChars(path2); return CombineNoChecks(path1, path2); } public static string Combine(string path1, string path2, string path3) { if (path1 == null || path2 == null || path3 == null) throw new ArgumentNullException((path1 == null) ? nameof(path1): (path2 == null) ? nameof(path2): nameof(path3)); Contract.EndContractBlock(); PathInternal.CheckInvalidPathChars(path1); PathInternal.CheckInvalidPathChars(path2); PathInternal.CheckInvalidPathChars(path3); return CombineNoChecks(path1, path2, path3); } public static string Combine(string path1, string path2, string path3, string path4) { if (path1 == null || path2 == null || path3 == null || path4 == null) throw new ArgumentNullException((path1 == null) ? nameof(path1): (path2 == null) ? nameof(path2): (path3 == null) ? nameof(path3): nameof(path4)); Contract.EndContractBlock(); PathInternal.CheckInvalidPathChars(path1); PathInternal.CheckInvalidPathChars(path2); PathInternal.CheckInvalidPathChars(path3); PathInternal.CheckInvalidPathChars(path4); return CombineNoChecks(path1, path2, path3, path4); } public static string Combine(params string[] paths) { if (paths == null) { throw new ArgumentNullException(nameof(paths)); } Contract.EndContractBlock(); int finalSize = 0; int firstComponent = 0; // We have two passes, the first calculates how large a buffer to allocate and does some precondition // checks on the paths passed in. The second actually does the combination. for (int i = 0; i < paths.Length; i++) { if (paths[i] == null) { throw new ArgumentNullException(nameof(paths)); } if (paths[i].Length == 0) { continue; } PathInternal.CheckInvalidPathChars(paths[i]); if (IsPathRooted(paths[i])) { firstComponent = i; finalSize = paths[i].Length; } else { finalSize += paths[i].Length; } char ch = paths[i][paths[i].Length - 1]; if (!PathInternal.IsDirectoryOrVolumeSeparator(ch)) finalSize++; } StringBuilder finalPath = StringBuilderCache.Acquire(finalSize); for (int i = firstComponent; i < paths.Length; i++) { if (paths[i].Length == 0) { continue; } if (finalPath.Length == 0) { finalPath.Append(paths[i]); } else { char ch = finalPath[finalPath.Length - 1]; if (!PathInternal.IsDirectoryOrVolumeSeparator(ch)) { finalPath.Append(PathInternal.DirectorySeparatorChar); } finalPath.Append(paths[i]); } } return StringBuilderCache.GetStringAndRelease(finalPath); } private static string CombineNoChecks(string path1, string path2) { if (path2.Length == 0) return path1; if (path1.Length == 0) return path2; if (IsPathRooted(path2)) return path2; char ch = path1[path1.Length - 1]; return PathInternal.IsDirectoryOrVolumeSeparator(ch) ? path1 + path2 : path1 + PathInternal.DirectorySeparatorCharAsString + path2; } private static string CombineNoChecks(string path1, string path2, string path3) { if (path1.Length == 0) return CombineNoChecks(path2, path3); if (path2.Length == 0) return CombineNoChecks(path1, path3); if (path3.Length == 0) return CombineNoChecks(path1, path2); if (IsPathRooted(path3)) return path3; if (IsPathRooted(path2)) return CombineNoChecks(path2, path3); bool hasSep1 = PathInternal.IsDirectoryOrVolumeSeparator(path1[path1.Length - 1]); bool hasSep2 = PathInternal.IsDirectoryOrVolumeSeparator(path2[path2.Length - 1]); if (hasSep1 && hasSep2) { return path1 + path2 + path3; } else if (hasSep1) { return path1 + path2 + PathInternal.DirectorySeparatorCharAsString + path3; } else if (hasSep2) { return path1 + PathInternal.DirectorySeparatorCharAsString + path2 + path3; } else { // string.Concat only has string-based overloads up to four arguments; after that requires allocating // a params string[]. Instead, try to use a cached StringBuilder. StringBuilder sb = StringBuilderCache.Acquire(path1.Length + path2.Length + path3.Length + 2); sb.Append(path1) .Append(PathInternal.DirectorySeparatorChar) .Append(path2) .Append(PathInternal.DirectorySeparatorChar) .Append(path3); return StringBuilderCache.GetStringAndRelease(sb); } } private static string CombineNoChecks(string path1, string path2, string path3, string path4) { if (path1.Length == 0) return CombineNoChecks(path2, path3, path4); if (path2.Length == 0) return CombineNoChecks(path1, path3, path4); if (path3.Length == 0) return CombineNoChecks(path1, path2, path4); if (path4.Length == 0) return CombineNoChecks(path1, path2, path3); if (IsPathRooted(path4)) return path4; if (IsPathRooted(path3)) return CombineNoChecks(path3, path4); if (IsPathRooted(path2)) return CombineNoChecks(path2, path3, path4); bool hasSep1 = PathInternal.IsDirectoryOrVolumeSeparator(path1[path1.Length - 1]); bool hasSep2 = PathInternal.IsDirectoryOrVolumeSeparator(path2[path2.Length - 1]); bool hasSep3 = PathInternal.IsDirectoryOrVolumeSeparator(path3[path3.Length - 1]); if (hasSep1 && hasSep2 && hasSep3) { // Use string.Concat overload that takes four strings return path1 + path2 + path3 + path4; } else { // string.Concat only has string-based overloads up to four arguments; after that requires allocating // a params string[]. Instead, try to use a cached StringBuilder. StringBuilder sb = StringBuilderCache.Acquire(path1.Length + path2.Length + path3.Length + path4.Length + 3); sb.Append(path1); if (!hasSep1) { sb.Append(PathInternal.DirectorySeparatorChar); } sb.Append(path2); if (!hasSep2) { sb.Append(PathInternal.DirectorySeparatorChar); } sb.Append(path3); if (!hasSep3) { sb.Append(PathInternal.DirectorySeparatorChar); } sb.Append(path4); return StringBuilderCache.GetStringAndRelease(sb); } } private static readonly char[] s_base32Char = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5'}; private static unsafe void Populate83FileNameFromRandomBytes(byte* bytes, int byteCount, char* chars, int charCount) { Debug.Assert(bytes != null); Debug.Assert(chars != null); // This method requires bytes of length 8 and chars of length 12. Debug.Assert(byteCount == 8, $"Unexpected {nameof(byteCount)}"); Debug.Assert(charCount == 12, $"Unexpected {nameof(charCount)}"); byte b0 = bytes[0]; byte b1 = bytes[1]; byte b2 = bytes[2]; byte b3 = bytes[3]; byte b4 = bytes[4]; // Consume the 5 Least significant bits of the first 5 bytes chars[0] = s_base32Char[b0 & 0x1F]; chars[1] = s_base32Char[b1 & 0x1F]; chars[2] = s_base32Char[b2 & 0x1F]; chars[3] = s_base32Char[b3 & 0x1F]; chars[4] = s_base32Char[b4 & 0x1F]; // Consume 3 MSB of b0, b1, MSB bits 6, 7 of b3, b4 chars[5] = s_base32Char[( ((b0 & 0xE0) >> 5) | ((b3 & 0x60) >> 2))]; chars[6] = s_base32Char[( ((b1 & 0xE0) >> 5) | ((b4 & 0x60) >> 2))]; // Consume 3 MSB bits of b2, 1 MSB bit of b3, b4 b2 >>= 5; Debug.Assert(((b2 & 0xF8) == 0), "Unexpected set bits"); if ((b3 & 0x80) != 0) b2 |= 0x08; if ((b4 & 0x80) != 0) b2 |= 0x10; chars[7] = s_base32Char[b2]; // Set the file extension separator chars[8] = '.'; // Consume the 5 Least significant bits of the remaining 3 bytes chars[9] = s_base32Char[(bytes[5] & 0x1F)]; chars[10] = s_base32Char[(bytes[6] & 0x1F)]; chars[11] = s_base32Char[(bytes[7] & 0x1F)]; } /// /// Create a relative path from one path to another. Paths will be resolved before calculating the difference. /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix). /// /// The source path the output should be relative to. This path is always considered to be a directory. /// The destination path. /// The relative path or if the paths don't share the same root. /// Thrown if or is null or an empty string. public static string GetRelativePath(string relativeTo, string path) { return GetRelativePath(relativeTo, path, StringComparison); } private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType) { if (string.IsNullOrEmpty(relativeTo)) throw new ArgumentNullException(nameof(relativeTo)); if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path)); Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase); relativeTo = GetFullPath(relativeTo); path = GetFullPath(path); // Need to check if the roots are different- if they are we need to return the "to" path. if (!PathInternal.AreRootsEqual(relativeTo, path, comparisonType)) return path; int commonLength = PathInternal.GetCommonPathLength(relativeTo, path, ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase); // If there is nothing in common they can't share the same root, return the "to" path as is. if (commonLength == 0) return path; // Trailing separators aren't significant for comparison int relativeToLength = relativeTo.Length; if (PathInternal.EndsInDirectorySeparator(relativeTo)) relativeToLength--; bool pathEndsInSeparator = PathInternal.EndsInDirectorySeparator(path); int pathLength = path.Length; if (pathEndsInSeparator) pathLength--; // If we have effectively the same path, return "." if (relativeToLength == pathLength && commonLength >= relativeToLength) return "."; // We have the same root, we need to calculate the difference now using the // common Length and Segment count past the length. // // Some examples: // // C:\Foo C:\Bar L3, S1 -> ..\Bar // C:\Foo C:\Foo\Bar L6, S0 -> Bar // C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar // C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar StringBuilder sb = StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length)); // Add parent segments for segments past the common on the "from" path if (commonLength < relativeToLength) { sb.Append(PathInternal.ParentDirectoryPrefix); for (int i = commonLength; i < relativeToLength; i++) { if (PathInternal.IsDirectorySeparator(relativeTo[i])) { sb.Append(PathInternal.ParentDirectoryPrefix); } } } else if (PathInternal.IsDirectorySeparator(path[commonLength])) { // No parent segments and we need to eat the initial separator // (C:\Foo C:\Foo\Bar case) commonLength++; } // Now add the rest of the "to" path, adding back the trailing separator int count = pathLength - commonLength; if (pathEndsInSeparator) count++; sb.Append(path, commonLength, count); return StringBuilderCache.GetStringAndRelease(sb); } // StringComparison and IsCaseSensitive are also available in PathInternal.CaseSensitivity but we are // too low in System.Runtime.Extensions to use it (no FileStream, etc.) /// Returns a comparison that can be used to compare file and directory names for equality. internal static StringComparison StringComparison { get { return IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; } } } }