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 || NET35 || NET40 || NET45)
///
/// 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(); }
}
}