4 * @brief Path handling routines
13 #pragma warning (push) // prevent "warning C4091: 'typedef ': ignored on left of 'tagGPFIDL_FLAGS' when no variable is declared"
14 #pragma warning (disable:4091) // VC bug when using XP enabled toolsets.
18 #include "PathContext.h"
19 #include "coretools.h"
25 static bool IsSlash(const String& pszStart, size_t nPos);
26 static bool IsDirName(const String& sDir);
29 * @brief Checks if char in string is slash.
30 * @param [in] pszStart String to check.
31 * @param [in] nPos of char in string to check (0-based index).
32 * @return true if char is slash.
34 static bool IsSlash(const String& pszStart, size_t nPos)
36 return pszStart[nPos]=='/' ||
41 * @brief Checks if string ends with slash.
42 * This function checks if given string ends with slash. In many places,
43 * especially in GUI, we assume folder paths end with slash.
44 * @param [in] s String to check.
45 * @return true if last char in string is slash.
47 bool EndsWithSlash(const String& s)
49 if (size_t len = s.length())
50 return IsSlash(s, (int)len - 1);
55 * @brief Checks if path exists and if it points to folder or file.
56 * This function first checks if path exists. If path exists
57 * then function checks if path points to folder or file.
58 * @param [in] szPath Path to check.
60 * - DOES_NOT_EXIST : path does not exists
61 * - IS_EXISTING_DIR : path points to existing folder
62 * - IS_EXISTING_FILE : path points to existing file
64 PATH_EXISTENCE DoesPathExist(const String& szPath, bool (*IsArchiveFile)(const String&) /*= nullptr*/)
67 return DOES_NOT_EXIST;
69 // Expand environment variables:
70 // Convert "%userprofile%\My Documents" to "C:\Documents and Settings\username\My Documents"
71 const tchar_t *lpcszPath = szPath.c_str();
72 tchar_t expandedPath[MAX_PATH_FULL];
74 if (tc::tcschr(lpcszPath, '%') != nullptr)
76 DWORD dwLen = ExpandEnvironmentStrings(lpcszPath, expandedPath, MAX_PATH_FULL);
77 if (dwLen > 0 && dwLen < MAX_PATH_FULL)
78 lpcszPath = expandedPath;
81 DWORD attr = GetFileAttributes(TFile(String(lpcszPath)).wpath().c_str());
83 if (attr == ((DWORD) -1))
85 if (IsArchiveFile && IsArchiveFile(szPath))
86 return IS_EXISTING_DIR;
87 return DOES_NOT_EXIST;
89 else if (attr & FILE_ATTRIBUTE_DIRECTORY)
90 return IS_EXISTING_DIR;
93 if (IsArchiveFile && IsArchiveFile(szPath))
94 return IS_EXISTING_DIR;
95 return IS_EXISTING_FILE;
100 * @brief Like shlwapi's PathFindFileName(), but works with both \ and /.
104 String FindFileName(const String& path)
106 const tchar_t *filename = path.c_str();
107 while (const tchar_t *slash = tc::tcspbrk(filename, _T("\\/")))
109 if (*(slash + 1) == '\0')
111 filename = slash + 1;
117 * @brief Like shlwapi's PathFindFileName(), but works with both \ and /.
121 String FindExtension(const String& path)
123 return ::PathFindExtension(path.c_str());
126 String RemoveExtension(const String& path)
128 String ext = FindExtension(path);
129 return path.substr(0, path.length() - ext.length());
133 * @brief Strip trailing slas.
134 * This function strips trailing slash from given path. Root paths are special
135 * case and they are left intact. Since C:\ is a valid path but C: is not.
136 * @param [in,out] sPath Path to strip.
138 void normalize(String & sPath)
140 size_t len = sPath.length();
144 // prefix root with current drive
145 sPath = GetLongPath(sPath);
147 // Do not remove trailing slash from root directories
148 if (len == 3 && sPath[1] == ':')
151 // remove any trailing slash
152 if (EndsWithSlash(sPath))
153 sPath.resize(sPath.length() - 1);
157 * @brief Returns whether the given path is a directory name
158 * @param [in] sDir Folder to handle.
159 * @return true the given path is a directory name
161 static bool IsDirName(const String& sDir)
163 // FindFirstFile doesn't work for root:
164 // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/findfirstfile.asp
165 // You cannot use root directories as the lpFileName input string for FindFirstFile - with or without a trailing backslash.
167 if ((sDir[0] && sDir[1] == ':' && sDir[2] == '\0') ||
168 // \\host\share or \\host\share\
169 (sDir[0] == '\\' && sDir[1] == '\\' &&
170 (count = std::count(sDir.begin(), sDir.end(), ('\\'))) <= 4 &&
171 (count == 3 || (count == 4 && sDir.back() == '\\'))))
173 // I don't know if this work for empty root directories
174 // because my first return value is not a dot directory, as I would have expected
176 HANDLE h = FindFirstFile((sDir + (sDir.back() == '\\' ? _T("*") : _T("\\*"))).c_str(), &ffd);
177 if (h == INVALID_HANDLE_VALUE)
182 // (Couldn't get info for just the directory from CFindFile)
185 HANDLE h = FindFirstFile(TFile(sDir).wpath().c_str(), &ffd);
186 if (h == INVALID_HANDLE_VALUE)
193 * Convert path to canonical long path.
194 * This function converts given path to canonical long form. For example
195 * foldenames with ~ short names are expanded. Also environment strings are
196 * expanded if @p bExpandEnvs is true. If path does not exist, make canonical
197 * the part that does exist, and leave the rest as is. Result, if a directory,
198 * usually does not have a trailing backslash.
199 * @param [in] sPath Path to convert.
200 * @param [in] bExpandEnvs If true environment variables are expanded.
201 * @return Converted path.
203 String GetLongPath(const String& szPath, bool bExpandEnvs)
205 String sPath = szPath;
206 size_t len = sPath.length();
210 tchar_t fullPath[MAX_PATH_FULL] = {0};
211 tchar_t *pFullPath = &fullPath[0];
214 // GetFullPathName GetLongPathName
215 // Convert to fully qualified form Yes No
217 // Convert /, //, \/, ... to \ Yes No
218 // Handle ., .., ..\..\.. Yes No
219 // Convert 8.3 names to long names No Yes
220 // Fail when file/directory does not exist No Yes
222 // Fully qualify/normalize name using GetFullPathName.
224 // Expand environment variables:
225 // Convert "%userprofile%\My Documents" to "C:\Documents and Settings\username\My Documents"
226 tchar_t expandedPath[MAX_PATH_FULL];
227 const tchar_t *lpcszPath = sPath.c_str();
228 if (bExpandEnvs && tc::tcschr(lpcszPath, '%') != nullptr)
230 DWORD dwLen = ExpandEnvironmentStrings(lpcszPath, expandedPath, MAX_PATH_FULL);
231 if (dwLen > 0 && dwLen < MAX_PATH_FULL)
232 lpcszPath = expandedPath;
235 String tPath = TFile(String(lpcszPath)).wpath();
236 DWORD dwLen = GetFullPathName(tPath.c_str(), MAX_PATH_FULL, pFullPath, &lpPart);
237 if (dwLen == 0 || dwLen >= MAX_PATH_FULL)
238 tc::tcslcpy(pFullPath, MAX_PATH_FULL, tPath.c_str());
240 // We are done if this is not a short name.
241 if (tc::tcschr(pFullPath, _T('~')) == nullptr)
244 // We have to do it the hard way because GetLongPathName is not
245 // available on Win9x and some WinNT 4
247 // The file/directory does not exist, use as much long name as we can
248 // and leave the invalid stuff at the end.
250 tchar_t *ptr = pFullPath;
251 tchar_t *end = nullptr;
253 // Skip to \ position d:\abcd or \\host\share\abcd
254 // indicated by ^ ^ ^
255 if (tc::tcslen(ptr) > 2)
256 end = tc::tcschr(pFullPath+2, _T('\\'));
257 if (end != nullptr && !tc::tcsncmp(pFullPath, _T("\\\\"),2))
258 end = tc::tcschr(end+1, _T('\\'));
267 // now walk down each directory and do short to long name conversion
268 while (ptr != nullptr)
270 end = tc::tcschr(ptr, '\\');
271 // zero-terminate current component
272 // (if we're at end, its already zero-terminated)
280 // advance to next component (or set ptr=`nullptr` to flag end)
281 ptr = (end!=nullptr ? end+1 : nullptr);
283 // (Couldn't get info for just the directory from CFindFile)
285 HANDLE h = FindFirstFile(TFile(sTemp).wpath().c_str(), &ffd);
286 if (h == INVALID_HANDLE_VALUE)
288 sLong = std::move(sTemp);
297 sLong += ffd.cFileName;
304 * @brief Check if the path exist and create the folder if needed.
305 * This function checks if path exists. If path does not yet exist
306 * function created needed folder structure. So this function is the
307 * easy way to create a needed folder structure. Environment strings are
308 * expanded when handling paths.
309 * @param [in] sPath Path to check/create.
310 * @return true if path exists or if we successfully created it.
312 bool CreateIfNeeded(const String& szPath)
317 if (IsDirName(szPath))
320 if (szPath.length() >= MAX_PATH_FULL)
323 // Expand environment variables:
324 // Convert "%userprofile%\My Documents" to "C:\Documents and Settings\username\My Documents"
325 tchar_t fullPath[MAX_PATH_FULL];
327 if (tc::tcschr(szPath.c_str(), '%') != nullptr)
329 DWORD dwLen = ExpandEnvironmentStrings(szPath.c_str(), fullPath, MAX_PATH_FULL);
330 if (dwLen == 0 || dwLen >= MAX_PATH_FULL)
331 tc::tcslcpy(fullPath, szPath.c_str());
334 tc::tcslcpy(fullPath, szPath.c_str());
335 // Now fullPath holds our desired path
337 tchar_t *ptr = fullPath;
338 tchar_t *end = nullptr;
340 // Skip to \ position d:\abcd or \\host\share\abcd
341 // indicated by ^ ^ ^
342 if (tc::tcslen(ptr) > 2)
343 end = tc::tcschr(fullPath+2, _T('\\'));
344 if (end != nullptr && !tc::tcsncmp(fullPath, _T("\\\\"),2))
345 end = tc::tcschr(end+1, _T('\\'));
347 if (end == nullptr) return false;
349 // check that first component exists
351 if (!IsDirName(fullPath))
357 while (ptr != nullptr)
359 end = tc::tcschr(ptr, '\\');
360 // zero-terminate current component
361 // (if we're at end, its already zero-terminated)
365 // advance to next component (or set ptr=`nullptr` to flag end)
366 ptr = (end != nullptr ? end+1 : nullptr);
368 if (!IsDirName(fullPath))
370 // try to create directory, and then double-check its existence
371 if (!CreateDirectory(fullPath, 0) ||
372 !IsDirName(fullPath))
377 // if not finished, restore directory string we're working in
385 * @brief Check if paths are both folders or files.
386 * This function checks if paths are "compatible" as in many places we need
387 * to have two folders or two files.
388 * @param [in] paths Left and right paths.
390 * - IS_EXISTING_DIR : both are directories & exist
391 * - IS_EXISTING_FILE : both are files & exist
392 * - DOES_NOT_EXIST : in all other cases
394 PATH_EXISTENCE GetPairComparability(const PathContext & paths, bool (*IsArchiveFile)(const String&) /*= nullptr*/)
396 // fail if not both specified
397 if (paths.GetSize() < 2 || paths[0].empty() || paths[1].empty())
398 return DOES_NOT_EXIST;
399 PATH_EXISTENCE p1 = DoesPathExist(paths[0], IsArchiveFile);
400 // short circuit testing right if left doesn't exist
401 if (p1 == DOES_NOT_EXIST)
402 return DOES_NOT_EXIST;
403 PATH_EXISTENCE p2 = DoesPathExist(paths[1], IsArchiveFile);
406 p1 = DoesPathExist(paths[0]);
407 p2 = DoesPathExist(paths[1]);
409 return DOES_NOT_EXIST;
411 if (paths.GetSize() < 3) return p1;
412 PATH_EXISTENCE p3 = DoesPathExist(paths[2], IsArchiveFile);
415 p1 = DoesPathExist(paths[0]);
416 p2 = DoesPathExist(paths[1]);
417 p3 = DoesPathExist(paths[2]);
418 if (p1 != p2 || p2 != p3)
419 return DOES_NOT_EXIST;
425 * @brief Check if the given path points to shotcut.
426 * Windows considers paths having a filename with extension ".lnk" as
427 * shortcuts. This function checks if the given path is shortcut.
428 * We usually want to expand shortcuts with ExpandShortcut().
429 * @param [in] inPath Path to check;
430 * @return true if the path points to shortcut, false otherwise.
432 bool IsShortcut(const String& inPath)
434 const tchar_t ShortcutExt[] = _T(".lnk");
435 tchar_t ext[_MAX_EXT] = {0};
436 _wsplitpath_s(inPath.c_str(), nullptr, 0, nullptr, 0, nullptr, 0, ext, _MAX_EXT);
437 if (tc::tcsicmp(ext, ShortcutExt) == 0)
443 bool IsDirectory(const String &path)
445 return !!PathIsDirectory(path.c_str());
448 //////////////////////////////////////////////////////////////////
449 // use IShellLink to expand the shortcut
450 // returns the expanded file, or "" on error
452 // original code was part of CShortcut
453 // 1996 by Rob Warner
454 // rhwarner@southeast.net
455 // http://users.southeast.net/~rhwarner
458 * @brief Expand given shortcut to full path.
459 * @param [in] inFile Shortcut to expand.
460 * @return Full path or empty string if error happened.
462 String ExpandShortcut(const String &inFile)
464 assert(!inFile.empty());
466 // No path, nothing to return
474 // Create instance for shell link
475 hres = ::CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
476 IID_IShellLink, (LPVOID*) &psl);
479 // Get a pointer to the persist file interface
481 hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*) &ppf);
484 WCHAR wsz[MAX_PATH_FULL];
485 tc::tcslcpy(wsz, inFile.c_str());
488 hres = ppf->Load(wsz, STGM_READ);
492 // find the path from that
493 tchar_t buf[MAX_PATH_FULL] = {0};
494 psl->GetPath(buf, MAX_PATH_FULL, nullptr, SLGP_UNCPRIORITY);
502 // if this fails, outFile == ""
507 * @brief Append subpath to path.
508 * This function appends subpath to given path. Function ensures there
509 * is only one backslash between path parts.
510 * @param [in] path "Base" path where other part is appended.
511 * @param [in] subpath Path part to append to base part.
512 * @return Formatted path. If one of arguments is empty then returns
513 * non-empty argument. If both argumets are empty empty string is returned.
515 String ConcatPath(const String & path, const String & subpath)
521 if (EndsWithSlash(path))
523 return String(path).append(subpath.c_str() + (IsSlash(subpath, 0) ? 1 : 0));
527 if (IsSlash(subpath, 0))
529 return path + subpath;
533 return path + _T("\\") + subpath;
539 * @brief Get parent path.
540 * This function returns parent path for given path. For example for
541 * path "c:\folder\subfolder" we return "c:\folder".
542 * @param [in] path Path to get parent path for.
543 * @return Parent path.
545 String GetParentPath(const String& path)
547 String parentPath(path);
548 size_t len = parentPath.length();
550 // Remove last '\' from paths
551 if (parentPath[len - 1] == '\\')
553 parentPath.resize(len - 1);
557 // Remove last part of path
558 size_t pos = parentPath.rfind('\\');
560 if (pos != parentPath.npos)
562 // Do not remove trailing slash from root directories
563 parentPath.resize(pos == 2 ? pos + 1 : pos);
569 * @brief Get last subdirectory of path.
571 * Returns last subdirectory name (if one exists) from given path.
573 * - C:\work\myproject returns \myproject
574 * @param [in] path Original path.
575 * @return Last subdirectory in path.
577 String GetLastSubdir(const String & path)
579 String parentPath(path);
580 size_t len = parentPath.length();
582 // Remove last '\' from paths
583 if (parentPath[len - 1] == '\\')
585 parentPath.erase(len - 1, 1);
589 // Find last part of path
590 size_t pos = parentPath.find_last_of('\\');
591 if (pos >= 2 && pos != String::npos)
592 parentPath.erase(0, pos);
597 * @brief Checks if path is an absolute path.
598 * @param [in] path Path to check.
599 * @return true if given path is absolute path.
601 bool IsPathAbsolute(const String &path)
603 if (path.length() < 3)
606 size_t pos = path.find_last_of('\\');
608 // Absolute path must have "\" and cannot start with it.
609 // Also "\\blahblah" is invalid.
610 if (pos < 2 || pos == String::npos)
613 // Maybe "X:\blahblah"?
614 if (path[1] == ':' && path[2] == '\\')
618 if (path[0] == '\\' && path[1] == '\\' && pos > 2)
625 * @brief Checks if folder exists and creates it if needed.
626 * This function checks if folder exists and creates it if not.
628 * @return Path if it exists or were created successfully. If
629 * path points to file or folder failed to create returns empty
632 String EnsurePathExist(const String & sPath)
634 int rtn = DoesPathExist(sPath);
635 if (rtn == IS_EXISTING_DIR)
637 if (rtn == IS_EXISTING_FILE)
639 if (!CreateIfNeeded(sPath))
641 // Check creating folder succeeded
642 if (DoesPathExist(sPath) == IS_EXISTING_DIR)
649 * @brief Return true if *pszChar is a slash (either direction) or a colon
651 * begin points to start of string, in case multibyte trail test is needed
653 bool IsSlashOrColon(const tchar_t *pszChar, const tchar_t *begin)
655 return (*pszChar == '/' || *pszChar == ':' || *pszChar == '\\');
659 * @brief Extract path name components from given full path.
660 * @param [in] pathLeft Original path.
661 * @param [out] pPath Folder name component of full path, excluding
663 * @param [out] pFile File name part, excluding extension.
664 * @param [out] pExt Filename extension part, excluding leading dot.
666 void SplitFilename(const String& pathLeft, String* pPath, String* pFile, String* pExt)
668 const tchar_t *pszChar = pathLeft.c_str() + pathLeft.length();
669 const tchar_t *pend = pszChar;
670 const tchar_t *extptr = 0;
673 while (pathLeft.c_str() < --pszChar)
681 (*pExt) = pszChar + 1;
683 ext = true; // extension is only after last period
687 else if (IsSlashOrColon(pszChar, pathLeft.c_str()))
689 // Ok, found last slash, so we collect any info desired
692 if (pPath != nullptr)
694 // Grab directory (omit trailing slash)
695 size_t len = pszChar - pathLeft.c_str();
697 ++len; // Keep trailing colon ( eg, C:filename.txt)
699 pPath->erase(len); // Cut rest of path
702 if (pFile != nullptr)
705 *pFile = pszChar + 1;
712 // Never found a delimiter
713 if (pFile != nullptr)
719 // if both filename & extension requested, remove extension from filename
721 if (pFile != nullptr && pExt != nullptr && extptr != nullptr)
723 size_t extlen = pend - extptr;
724 pFile->erase(pFile->length() - extlen);
729 * @brief Return path component from full path.
730 * @param [in] fullpath Full path to split.
731 * @return Path without filename.
733 String GetPathOnly(const String& fullpath)
735 if (fullpath.empty()) return _T("");
737 SplitFilename(fullpath, &spath, 0, 0);
741 bool IsURL(const String& abspath)
743 for (size_t i = 0; i < abspath.length(); ++i)
745 const auto c = abspath[i];
746 if (c == '\\' || c == '/')
748 // If there is a \ or / before the : character, consider it not a URL.
757 bool IsURLorCLSID(const String& path)
759 return IsURL(path) || path.find(_T("::{")) != String::npos;
762 bool isFileURL(const String& path)
764 return UrlIsFileUrl(path.c_str());
767 String FromURL(const String& url)
769 std::vector<tchar_t> path((std::max)(size_t(MAX_PATH), url.length() + 1));
770 DWORD size = static_cast<DWORD>(path.size());
771 PathCreateFromUrl(url.c_str(), path.data(), &size, 0);
775 bool IsDecendant(const String& path, const String& ancestor)
777 return path.length() > ancestor.length() &&
778 strutils::compare_nocase(String(path.c_str(), path.c_str() + ancestor.length()), ancestor) == 0;
781 static void replace_char(tchar_t *s, int target, int repl)
784 for (p=s; *p != _T('\0'); p = tc::tcsinc(p))
789 String ToWindowsPath(const String& path)
791 String winpath = path;
792 replace_char(&*winpath.begin(), '/', '\\');
796 String ToUnixPath(const String& path)
798 String unixpath = path;
799 replace_char(&*unixpath.begin(), '\\', '/');
804 * @brief Return whether whether the given name can be used as a filename or directory name.
805 * This function performs a test PathGetCharType() on each character in the specified name.
806 * @param [in] name Filename or directory name to check.
807 * @return true if the given name can be used as a filename or directory name.
809 bool IsValidName(const String& name)
811 for (String::const_iterator it = name.begin(); it != name.end(); ++it)
812 if (!(PathGetCharType(*it) & GCT_LFNCHAR))