using System; using System.Collections.Generic; using System.Linq; using System.Text; using Vanara.Extensions; using Vanara.PInvoke; using static Vanara.PInvoke.Kernel32; using static Vanara.PInvoke.ShlwApi; namespace Vanara.IO { /// /// Performs operations on String instances that contain file or directory path information. These operations are performed in a cross-platform manner. /// public static class PathEx { /// The type of character retrieved from . [Flags] public enum PathCharType : uint { /// The character is not valid in a path. Invalid = 0x0000, /// The character is valid in a long file name. LongFileName = 0x0001, /// The character is valid in a short (8.3) file name. ShortFileName = 0x0002, /// The character is a wildcard character. Wildcard = 0x0004, /// The character is a path separator. Separator = 0x0008, } /// /// Adds a backslash to the end of a string to create the correct syntax for a path. If the source path already has a trailing /// backslash, no backslash will be added. /// /// The path string. This value should not be . /// The path string with the appended backslash. public static string AddBackslash(string pszPath) => SBAllocCallRet((s, sz) => PathCchAddBackslash(s, sz), pszPath, err: ThrowIfNotOkOrFalse); /// /// Adds a file name extension to a path string. /// /// The path of the file. This value should not be . /// The extension to add. This string can be given either with or without a preceding period (".ext" or "ext"). /// The string with the appended extension. public static string AddExtension(string path, string ext) => SBAllocCallRet((s, sz) => PathCchAddExtension(s, sz, ext), path, err: ThrowIfNotOkOrFalse); /// /// Converts a path string into a canonical form. /// /// The file or directory to canonicalize. /// The canonicalized path string. public static string Canonicalize(string path) { PathAllocCanonicalize(path, PATHCCH_OPTIONS.PATHCCH_ALLOW_LONG_PATHS, out var s).ThrowIfFailed(); return s; } /// Truncates a path to fit within a certain number of characters by replacing path components with ellipses. /// The file or directory to compact. /// The maximum number of characters to be contained in the new string. /// The altered, compacted path string. public static string Compact(string path, int maxChars) => SBAllocCallRet((s, sz) => PathCompactPathEx(s, path, (uint)maxChars + 1)); #if NET20_OR_GREATER /// /// Truncates a file path to fit within a given pixel width by replacing path components with ellipses. /// /// The file or directory to compact. /// The width, in pixels, in which the string must fit. /// A device context used for font metrics. This value can be . /// The modified string. public static string Compact(string path, int pixelWidth, System.Drawing.IDeviceContext dc) => SBAllocCallRet((s, sz) => PathCompactPath(dc?.GetHdc() ?? default, s, (uint)pixelWidth), path, (uint)(path?.Length + 1 ?? MAX_PATH)); #endif /// Converts a file URL to a Microsoft MS-DOS path. /// The URL. /// The resulting MS-DOS path. public static string CreateFromUrl(string url) => SBAllocCallRet((s, sz) => PathCreateFromUrl(url, s, ref sz)); /// Searches for a file in a list of directories. /// The file name for which to search. /// /// An optional array of directories to be searched first. This value can be . If no directories are specified, /// it attempts to find the file by searching standard directories such as System32 and the directories specified in the PATH /// environment variable. /// /// If the file is found, this variable holds the fully qualified path name of the found file. /// Returns if found, or otherwise. public static bool FileExistsOnPath(string fileName, IEnumerable searchDirectories, out string foundFilePath) { var sb = new StringBuilder(fileName, MAX_PATH); string[] dirs = searchDirectories?.ToArray(); if (dirs != null) { Array.Resize(ref dirs, dirs.Length + 1); dirs[dirs.Length - 1] = null; } foundFilePath = PathFindOnPath(sb, dirs) ? sb.ToString() : null; return foundFilePath != null; } /// Determines the type of character in relation to a path string. /// The character for which to determine the type. /// The type of character. public static PathCharType GetCharType(char ch) => (PathCharType)PathGetCharType(ch); /// Searches a path for a drive letter within the range of 'A' to 'Z' and returns the corresponding drive number. /// The file or directory to assess. /// Returns 0 through 25 (corresponding to 'A' through 'Z') if the path has a drive letter, or -1 otherwise. public static int GetDriveNumber(string path) => PathGetDriveNumber(path); /// Removes the file name extension from a path, if one is present. /// The path string to edit. /// The path with any extension removed. If no extension was found, the string is unchanged. public static string GetPathWithoutExtension(string path) => SBAllocCallRet((s, sz) => PathCchRemoveExtension(s, sz), path, err: ThrowIfNotOkOrFalse); /// /// Removes the last element in a path string, whether that element is a file name or a directory name. The element's leading /// backslash is also removed. /// /// The path string to edit. /// /// The path with its last element and its leading backslash removed. This function does not affect root paths such as "C:". In the /// case of a root path, the path string is returned unaltered. If a path string ends with a trailing backslash, only that backslash /// is removed. /// public static string GetPathWithoutLastElement(string path) => SBAllocCallRet((s, sz) => PathCchRemoveFileSpec(s, sz), path, err: ThrowIfNotOkOrFalse); /// /// Gets the string following the drive letter or Universal Naming Convention (UNC) server/share path elements in a path. /// /// The path string. /// /// The string following the drive letter or UNC server/share path elements. If the path consists of only a root, this value will be /// . /// public static string GetPathWithoutRoot(string path) { PathCchSkipRoot(path, out var ptr).ThrowIfFailed(); return StringHelper.GetString(ptr, System.Runtime.InteropServices.CharSet.Unicode); } /// Gets the path without the trailing backslash from the end of a path string. /// The path string. /// /// The path with any trailing backslash removed. If no trailing backslash was found, the string is the same as . /// public static string GetPathWithoutTrailingBackslash(string path) => SBAllocCallRet((s, sz) => PathCchRemoveBackslash(s, sz), path, (uint)path.Length + 1, ThrowIfNotOkOrFalse); /// Gets a relative path from one file or folder to another. /// The path that defines the start of the relative path. /// If , assume that is a directory. /// The path that defines the endpoint of the relative path. /// If , assume that is a directory. /// The relative path of from . /// /// This function takes a pair of paths and generates a relative path from one to the other. The paths do not have to be fully /// qualified, but they must have a common prefix, or the function will fail and return . /// /// For example, let the starting point, , be "c:\FolderA\FolderB\FolderC", and the ending point, /// , be "c:\FolderA\FolderD\FolderE". GetRelativePath will return the relative path from /// to as: "....\FolderD\FolderE". You will get the same result if you set /// to "\FolderA\FolderB\FolderC" and to "\FolderA\FolderD\FolderE". On the other hand, /// "c:\FolderA\FolderB" and "a:\FolderA\FolderD" do not share a common prefix, and the function will fail. Note that "\" is not /// considered a prefix and is ignored. If you set to "\FolderA\FolderB", and /// to "\FolderC\FolderD", the function will return . /// /// public static string GetRelativePath(string fromPath, bool fromIsDir, string toPath, bool toIsDir) => SBAllocCallRet((s, sz) => PathRelativePathTo(s, fromPath, fromIsDir ? PInvoke.FileFlagsAndAttributes.FILE_ATTRIBUTE_EA : 0, toPath, toIsDir ? PInvoke.FileFlagsAndAttributes.FILE_ATTRIBUTE_DIRECTORY : 0)); /// Creates a root path from a given drive number. /// A variable that indicates the desired drive number. It should be between 0 and 25. /// /// The constructed root path. If the call fails for any reason (for example, an invalid drive number), is returned. /// public static string GetRootFromDriveNumber(int drive) => SBAllocCallRet((s, sz) => PathBuildRoot(s, drive), "", 4); /// Gets the short path form of the specified path. /// The path to convert. /// The short form of . public static string GetShortPath(string path) { if (string.IsNullOrEmpty(path)) return path; var shortPath = path; var l = path.Length + 5; var sb = new StringBuilder(l, l); var rl = GetShortPathName(path.Length > MAX_PATH ? @"\\?\" + path : path, sb, (uint)sb.Capacity); if (rl > 0 && rl <= l) shortPath = sb.ToString(); return shortPath; } /// /// Determines if a path string is a valid Universal Naming Convention (UNC) path and extract the name of the server from the path. /// /// The UNC path. /// On success, the server portion of the UNC path. On failure, is returned. public static string GetUNCServer(string path) => PathIsUNCEx(path, out var ptr) ? StringHelper.GetString(ptr, System.Runtime.InteropServices.CharSet.Unicode, (path.Length + 1) * 2) : null; /// /// Searches a path to determine if it contains a valid prefix of the type passed by . A prefix is one of /// these types: "C:\", ".", "..", "..\". /// /// The file or directory path. /// The prefix for which to search. /// true if the specified path has the supplied prefix; otherwise, false. public static bool HasPrefix(string path, string prefix) => PathIsPrefix(prefix, path); /// Determines whether a given file name has one of a list of suffixes. /// The file path. /// The list of suffixes to check. /// /// A string with the matching suffix if successful, or if does not end with one of /// the specified suffixes. /// public static string HasSuffix(string path, string[] suffixes) => Vanara.Extensions.StringHelper.GetString(PathFindSuffixArray(path, suffixes, suffixes.Length)); /// Compares two paths to determine if they have a common root component. /// A string of maximum length MAX_PATH that contains the first path to be compared. /// A string of maximum length MAX_PATH that contains the second path to be compared. /// /// Returns if both strings have the same root component, or otherwise. If /// contains only the server and share, this function also returns . /// public static bool HaveSameRoot(string path1, string path2) => PathIsSameRoot(path1, path2); /// /// Determines if a file's registered content type matches the specified content type. This function obtains the content type for the /// specified file type and compares that string with the . The comparison is not case-sensitive. /// /// A string of maximum length MAX_PATH that contains the file whose content type will be compared. /// The content type string to which the file's registered content type will be compared. /// /// Returns if the file's registered content type matches , or /// otherwise. /// public static bool IsContentType(string path, string contentType) => PathIsContentType(path, contentType); /// Verifies that a path is a valid directory. /// A string of maximum length MAX_PATH that contains the path to verify. /// Returns if the path is a valid directory; otherwise, . public static bool IsDirectory(string path) => PathIsDirectory(path); /// Determines whether a specified path is an empty directory. /// A string of maximum length MAX_PATH that contains the path to be tested. /// /// Returns if is an empty directory. Returns if /// is not a directory, or if it contains at least one file other than "." or "..". /// /// "C:" is considered a directory. public static bool IsEmptyDirectory(string path) => PathIsDirectoryEmpty(path); /// /// Searches a path for any path-delimiting characters (for example, ':' or '' ). If there are no path-delimiting characters present, /// the path is considered to be a File Spec path. /// /// A string of maximum length MAX_PATH that contains the path to be searched. /// /// Returns if there are no path-delimiting characters within the path, or if there /// are path-delimiting characters. /// public static bool IsFileSpec(string path) => PathIsFileSpec(path); /// Determines whether a path string represents a network resource. /// A string of maximum length MAX_PATH that contains the path. /// Returns if the string represents a network resource, or otherwise. public static bool IsNetworkPath(string path) => PathIsNetworkPath(path); /// Searches a path and determines if it is relative. /// A string of maximum length MAX_PATH that contains the path to be searched. /// Returns if the path is relative, or if it is absolute. public static bool IsRelative(string path) => PathIsRelative(path); /// Determines whether a file name is in long format. /// A string of maximum length MAX_PATH that contains the file name to be tested. /// /// Returns if exceeds the number of characters allowed by the 8.3 format, or /// otherwise. /// public static bool IsShortFileNameCompatible(string path) => !PathIsLFNFileSpec(path); /// Determines if an existing folder contains the attributes that make it a system folder. /// /// A string of maximum length MAX_PATH that contains the name of an existing folder. The attributes for this folder will be /// retrieved and compared with those that define a system folder. If this folder contains the attributes to make it a system folder, /// the function returns . /// /// /// Returns if the represents a system folder, or otherwise. /// public static bool IsSystemFolder(string path) => PathIsSystemFolder(path, 0); /// /// Determines if a path string is a valid Universal Naming Convention (UNC) path, as opposed to a path based on a drive letter. /// /// A string of maximum length MAX_PATH that contains the path to validate. /// Returns if the string is a valid UNC path; otherwise, . public static bool IsUNC(string path) => PathIsUNC(path); /// Determines if a string is a valid Universal Naming Convention (UNC) for a server path only. /// A string of maximum length MAX_PATH that contains the path to validate. /// /// Returns if the string is a valid UNC path for a server only (no share name), or otherwise. /// public static bool IsUNCServer(string path) => PathIsUNCServer(path); /// Determines if a string is a valid Universal Naming Convention (UNC) share path, \server\share. /// A string of maximum length MAX_PATH that contains the path to be validated. /// Returns if the string is in the form \server\share, or otherwise. public static bool IsUNCServerShare(string path) => PathIsUNCServerShare(path); /// Tests a given string to determine if it conforms to a valid URL format. /// A string of maximum length MAX_PATH that contains the URL path to validate. /// Returns if has a valid URL format, or otherwise. /// This function does not verify that the path points to an existing site—only that it has a valid URL format. public static bool IsURL(string path) => PathIsURL(path); /// Gives an existing folder the proper attributes to become a system folder. /// /// A string of length MAX_PATH that contains the name of an existing folder that will be made into a system folder. /// /// Returns if successful, or otherwise. public static bool MakeSystemFolder(string path) => PathMakeSystemFolder(path); /// Matches a file name from a path against one or more file name patterns. /// A string of maximum length MAX_PATH that contains the path from which the file name to be matched is taken. /// /// A string of maximum length MAX_PATH that contains the file name pattern for which to search. This can be the exact name, or it /// can contain wildcard characters. This value can contain one pattern or multiple patterns separated with semicolons. /// /// /// If , ignore leading spaces in the string pointed to by . /// /// /// Return if a file name pattern specified in matched the file name found in the /// string pointed to by . /// public static bool MatchesLookupPattern(string path, string pattern, bool ignorePatternWhitespace = false) => PathMatchSpecEx(path, pattern, (ignorePatternWhitespace ? PMSF.PMSF_DONT_STRIP_SPACES : 0) | (pattern.IndexOf(';') < 0 ? PMSF.PMSF_NORMAL : PMSF.PMSF_MULTIPLE)) == HRESULT.S_OK; /// Searches a path for spaces. If spaces are found, the entire path is enclosed in quotation marks. /// A string that contains the path to search. /// A quoted string if spaces were found; otherwise, the value of . public static string QuoteIfHasSpaces(string path) => SBAllocCallRet((s, sz) => PathQuoteSpaces(s), path); /// /// Replaces a file name's extension at the end of a path string with a new extension. If the path string does not end with an /// extension, the new extension is added. /// /// A path string. /// /// A new extension string. The leading '.' character is optional. In the case of an empty string (""), any existing extension in the /// path string is removed. /// /// If this function succeeds, it returns with the renamed or added extension. public static string RenameExtension(string path, string newExt) => SBAllocCallRet((s, sz) => PathCchRenameExtension(s, sz, newExt), path); /// Removes the decoration from a path string. /// A string of length MAX_PATH that contains the path. /// The undecorated string. /// /// /// A decoration consists of a pair of square brackets with one or more digits in between, inserted immediately after the base name /// and before the file name extension. /// /// Examples /// The following table illustrates how strings are modified by Undecorate. /// /// /// Initial String /// Undecorated String /// /// /// C:\Path\File[5].txt /// C:\Path\File.txt /// /// /// C:\Path\File[12] /// C:\Path\File /// /// /// C:\Path\File.txt /// C:\Path\File.txt /// /// /// C:\Path\[3].txt /// C:\Path\[3].txt /// /// /// public static string Undecorate(string path) => SBAllocCallRet((s, sz) => PathUndecorate(s), path); /// Replaces certain folder names in a fully qualified path with their associated environment string. /// A string of maximum length MAX_PATH that contains the path to be unexpanded. /// Returns if successful; otherwise, . /// /// The following folder paths are replaced by their equivalent environment string. /// /// /// Folder /// Environment String /// /// /// The All Users profile folder /// %ALLUSERSPROFILE% /// /// /// The current user's application data folder (Windows Vista and later only). /// %APPDATA% /// /// /// The system name /// %COMPUTERNAME% /// /// /// The Program Files folder /// %ProgramFiles% /// /// /// The system root folder /// %SystemRoot% /// /// /// The system drive letter /// %SystemDrive% /// /// /// The current user's profile folder /// %USERPROFILE% /// /// /// /// Note %APPDATA% and %USERPROFILE% are relative to the user making the call. This function does not work if the user is /// being impersonated from a service. For further discussion of access control issues, see Access Control. /// /// /// The environment variables listed in the above table might not all be set on all systems. If an environment variable is not set, /// it is not unexpanded. /// /// public static string UnexpandEnvironmentStrings(string path) => SBAllocCallRet((s, sz) => PathUnExpandEnvStrings(path, s, sz)); /// /// Removes the attributes from a folder that make it a system folder. This folder must actually exist in the file system. /// /// /// A string of maximum length MAX_PATH that contains the name of an existing folder that will have the system folder attributes removed. /// /// Returns if successful, or otherwise. public static bool UnmakeSystemFolder(string path) => PathUnmakeSystemFolder(path); /// Removes quotes from the beginning and end of a path. /// A string of length MAX_PATH that contains the path. /// A string with beginning and ending quotation marks removed. public static string Unquote(string path) => SBAllocCallRet((s, sz) => PathUnquoteSpaces(s), path); private static string SBAllocCallRet(Action func, string inval = "", uint sz = MAX_PATH) { if (inval == null) throw new ArgumentNullException("path"); var sb = new StringBuilder((int)sz, (int)sz); sb.Append(inval); // Done this way in case the input string is longer than the buffer. func(sb, sz); return sb.ToString(); } private static string SBAllocCallRet(Func func, string inval = "", uint sz = PATHCCH_MAX_CCH, Action err = null) { if (inval == null) throw new ArgumentNullException("path"); var sb = new StringBuilder(inval, (int)sz); var hr = func(sb, sz); if (err != null) err?.Invoke(hr); else hr.ThrowIfFailed(); return sb.ToString(); } private static void ThrowIfNotOkOrFalse(HRESULT hr) { if (hr.Failed && hr != HRESULT.S_FALSE) hr.ThrowIfFailed(); } } }