4 * @brief Path handling routines
12 #include <mbctype.h> // MBCS (multibyte codepage stuff)
15 #include "PathContext.h"
16 #include "coretools.h"
21 static bool IsSlash(const String& pszStart, size_t nPos);
22 static bool GetDirName(const String& sDir, String& sName);
25 * @brief Checks if char in string is slash.
26 * @param [in] pszStart String to check.
27 * @param [in] nPos of char in string to check (0-based index).
28 * @return true if char is slash.
30 static bool IsSlash(const String& pszStart, size_t nPos)
32 return pszStart[nPos]=='/' ||
36 // Avoid 0x5C (ASCII backslash) byte occurring as trail byte in MBCS
37 (pszStart[nPos]=='\\' && !_ismbstrail((unsigned char *)pszStart.c_str(), (unsigned char *)pszStart.c_str() + nPos));
42 * @brief Checks if string ends with slash.
43 * This function checks if given string ends with slash. In many places,
44 * especially in GUI, we assume folder paths end with slash.
45 * @param [in] s String to check.
46 * @return true if last char in string is slash.
48 bool EndsWithSlash(const String& s)
50 if (size_t len = s.length())
51 return IsSlash(s, (int)len - 1);
56 * @brief Checks if path exists and if it points to folder or file.
57 * This function first checks if path exists. If path exists
58 * then function checks if path points to folder or file.
59 * @param [in] szPath Path to check.
61 * - DOES_NOT_EXIST : path does not exists
62 * - IS_EXISTING_DIR : path points to existing folder
63 * - IS_EXISTING_FILE : path points to existing file
65 PATH_EXISTENCE DoesPathExist(const String& szPath, bool (*IsArchiveFile)(const String&))
68 return DOES_NOT_EXIST;
70 // Expand environment variables:
71 // Convert "%userprofile%\My Documents" to "C:\Documents and Settings\username\My Documents"
72 const TCHAR *lpcszPath = szPath.c_str();
73 TCHAR expandedPath[_MAX_PATH];
75 if (_tcschr(lpcszPath, '%'))
77 DWORD dwLen = ExpandEnvironmentStrings(lpcszPath, expandedPath, _MAX_PATH);
78 if (dwLen > 0 && dwLen < _MAX_PATH)
79 lpcszPath = expandedPath;
82 DWORD attr = GetFileAttributes(lpcszPath);
84 if (attr == ((DWORD) -1))
86 if (IsArchiveFile && IsArchiveFile(szPath))
87 return IS_EXISTING_DIR;
88 return DOES_NOT_EXIST;
90 else if (attr & FILE_ATTRIBUTE_DIRECTORY)
91 return IS_EXISTING_DIR;
94 if (IsArchiveFile && IsArchiveFile(szPath))
95 return IS_EXISTING_DIR;
96 return IS_EXISTING_FILE;
101 * @brief Like shlwapi's PathFindFileName(), but works with both \ and /.
105 String FindFileName(const String& path)
107 const TCHAR *filename = path.c_str();
108 while (const TCHAR *slash = _tcspbrk(filename, _T("\\/")))
110 if (*(slash + 1) == '\0')
112 filename = slash + 1;
118 * @brief Like shlwapi's PathFindFileName(), but works with both \ and /.
122 String FindExtension(const String& path)
124 return ::PathFindExtension(path.c_str());
128 * @brief Strip trailing slas.
129 * This function strips trailing slas from given path. Root paths are special
130 * case and they are left intact. Since C:\ is a valid path but C: is not.
131 * @param [in,out] sPath Path to strip.
133 void normalize(String & sPath)
135 size_t len = sPath.length();
139 // prefix root with current drive
140 sPath = GetLongPath(sPath);
142 // Do not remove trailing slash from root directories
143 if (len == 3 && sPath[1] == ':')
146 // remove any trailing slash
147 if (EndsWithSlash(sPath))
148 sPath.resize(sPath.length() - 1);
152 * @brief Get canonical name of folder.
153 * @param [in] sDir Folder to handle.
154 * @param [out] sName Canonicalized folder name.
155 * @return true if canonical name exists.
156 * @todo Should we return empty string as sName when returning false?
158 static bool GetDirName(const String& sDir, String& sName)
160 // FindFirstFile doesn't work for root:
161 // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/findfirstfile.asp
162 // You cannot use root directories as the lpFileName input string for FindFirstFile
\97with or without a trailing backslash.
163 if (sDir[0] && sDir[1] == ':' && sDir[2] == '\0')
165 // I don't know if this work for empty root directories
166 // because my first return value is not a dot directory, as I would have expected
169 StringCchPrintf(sPath, sizeof(sPath)/sizeof(sPath[0]), _T("%s\\*"), sDir.c_str());
170 HANDLE h = FindFirstFile(sPath, &ffd);
171 if (h == INVALID_HANDLE_VALUE)
177 // (Couldn't get info for just the directory from CFindFile)
179 HANDLE h = FindFirstFile(sDir.c_str(), &ffd);
180 if (h == INVALID_HANDLE_VALUE)
182 sName = ffd.cFileName;
188 * Convert path to canonical long path.
189 * This function converts given path to canonical long form. For example
190 * foldenames with ~ short names are expanded. Also environment strings are
191 * expanded if @p bExpandEnvs is true. If path does not exist, make canonical
192 * the part that does exist, and leave the rest as is. Result, if a directory,
193 * usually does not have a trailing backslash.
194 * @param [in] sPath Path to convert.
195 * @param [in] bExpandEnvs If true environment variables are expanded.
196 * @return Converted path.
198 String GetLongPath(const String& szPath, bool bExpandEnvs)
200 String sPath = szPath;
201 size_t len = sPath.length();
205 TCHAR fullPath[_MAX_PATH] = {0};
208 // GetFullPathName GetLongPathName
209 // Convert to fully qualified form Yes No
211 // Convert /, //, \/, ... to \ Yes No
212 // Handle ., .., ..\..\.. Yes No
213 // Convert 8.3 names to long names No Yes
214 // Fail when file/directory does not exist No Yes
216 // Fully qualify/normalize name using GetFullPathName.
218 // Expand environment variables:
219 // Convert "%userprofile%\My Documents" to "C:\Documents and Settings\username\My Documents"
220 TCHAR expandedPath[_MAX_PATH];
221 const TCHAR *lpcszPath = sPath.c_str();
222 if (bExpandEnvs && _tcschr(lpcszPath, '%'))
224 DWORD dwLen = ExpandEnvironmentStrings(lpcszPath, expandedPath, _MAX_PATH);
225 if (dwLen > 0 && dwLen < _MAX_PATH)
226 lpcszPath = expandedPath;
229 DWORD dwLen = GetFullPathName(lpcszPath, _MAX_PATH, fullPath, &lpPart);
230 if (dwLen == 0 || dwLen >= _MAX_PATH)
231 _tcscpy_safe(fullPath, lpcszPath);
233 // We are done if this is not a short name.
234 if (_tcschr(fullPath, _T('~')) == NULL)
237 // We have to do it the hard way because GetLongPathName is not
238 // available on Win9x and some WinNT 4
240 // The file/directory does not exist, use as much long name as we can
241 // and leave the invalid stuff at the end.
243 TCHAR *ptr = fullPath;
246 // Skip to \ position d:\abcd or \\host\share\abcd
247 // indicated by ^ ^ ^
248 if (_tcslen(ptr) > 2)
249 end = _tcschr(fullPath+2, _T('\\'));
250 if (end && !_tcsncmp(fullPath, _T("\\\\"),2))
251 end = _tcschr(end+1, _T('\\'));
260 // now walk down each directory and do short to long name conversion
263 end = _tcschr(ptr, '\\');
264 // zero-terminate current component
265 // (if we're at end, its already zero-terminated)
273 // advance to next component (or set ptr==0 to flag end)
274 ptr = (end ? end+1 : 0);
276 // (Couldn't get info for just the directory from CFindFile)
278 HANDLE h = FindFirstFile(sTemp.c_str(), &ffd);
279 if (h == INVALID_HANDLE_VALUE)
290 sLong += ffd.cFileName;
297 * @brief Check if the path exist and create the folder if needed.
298 * This function checks if path exists. If path does not yet exist
299 * function created needed folder structure. So this function is the
300 * easy way to create a needed folder structure. Environment strings are
301 * expanded when handling paths.
302 * @param [in] sPath Path to check/create.
303 * @return true if path exists or if we successfully created it.
305 bool CreateIfNeeded(const String& szPath)
311 if (GetDirName(szPath, sTemp))
314 if (szPath.length() >= _MAX_PATH)
317 // Expand environment variables:
318 // Convert "%userprofile%\My Documents" to "C:\Documents and Settings\username\My Documents"
319 TCHAR fullPath[_MAX_PATH];
320 if (_tcschr(szPath.c_str(), '%'))
322 DWORD dwLen = ExpandEnvironmentStrings(szPath.c_str(), fullPath, _MAX_PATH);
323 if (dwLen == 0 || dwLen >= _MAX_PATH)
324 _tcscpy_safe(fullPath, szPath.c_str());
327 _tcscpy_safe(fullPath, szPath.c_str());
328 // Now fullPath holds our desired path
330 TCHAR *ptr = fullPath;
333 // Skip to \ position d:\abcd or \\host\share\abcd
334 // indicated by ^ ^ ^
335 if (_tcslen(ptr) > 2)
336 end = _tcschr(fullPath+2, _T('\\'));
337 if (end && !_tcsncmp(fullPath, _T("\\\\"),2))
338 end = _tcschr(end+1, _T('\\'));
340 if (!end) return false;
342 // check that first component exists
344 if (!GetDirName(fullPath, sTemp))
352 end = _tcschr(ptr, '\\');
353 // zero-terminate current component
354 // (if we're at end, its already zero-terminated)
358 // advance to next component (or set ptr==0 to flag end)
359 ptr = (end ? end+1 : 0);
362 if (!GetDirName(fullPath, sNextName))
364 // try to create directory, and then double-check its existence
365 if (!CreateDirectory(fullPath, 0) ||
366 !GetDirName(fullPath, sNextName))
371 // if not finished, restore directory string we're working in
379 * @brief Check if paths are both folders or files.
380 * This function checks if paths are "compatible" as in many places we need
381 * to have two folders or two files.
382 * @param [in] paths Left and right paths.
384 * - IS_EXISTING_DIR : both are directories & exist
385 * - IS_EXISTING_FILE : both are files & exist
386 * - DOES_NOT_EXIST : in all other cases
388 PATH_EXISTENCE GetPairComparability(const PathContext & paths, bool (*IsArchiveFile)(const String&))
390 // fail if not both specified
391 if (paths.GetSize() < 2 || paths[0].empty() || paths[1].empty())
392 return DOES_NOT_EXIST;
393 PATH_EXISTENCE p1 = DoesPathExist(paths[0], IsArchiveFile);
394 // short circuit testing right if left doesn't exist
395 if (p1 == DOES_NOT_EXIST)
396 return DOES_NOT_EXIST;
397 PATH_EXISTENCE p2 = DoesPathExist(paths[1], IsArchiveFile);
400 p1 = DoesPathExist(paths[0]);
401 p2 = DoesPathExist(paths[1]);
403 return DOES_NOT_EXIST;
405 if (paths.GetSize() < 3) return p1;
406 PATH_EXISTENCE p3 = DoesPathExist(paths[2], IsArchiveFile);
409 p1 = DoesPathExist(paths[0]);
410 p2 = DoesPathExist(paths[1]);
411 p3 = DoesPathExist(paths[2]);
412 if (p1 != p2 || p2 != p3)
413 return DOES_NOT_EXIST;
419 * @brief Check if the given path points to shotcut.
420 * Windows considers paths having a filename with extension ".lnk" as
421 * shortcuts. This function checks if the given path is shortcut.
422 * We usually want to expand shortcuts with ExpandShortcut().
423 * @param [in] inPath Path to check;
424 * @return true if the path points to shortcut, false otherwise.
426 bool IsShortcut(const String& inPath)
428 const TCHAR ShortcutExt[] = _T(".lnk");
429 TCHAR ext[_MAX_EXT] = {0};
430 _tsplitpath(inPath.c_str(), NULL, NULL, NULL, ext);
431 if (_tcsicmp(ext, ShortcutExt) == 0)
437 bool IsDirectory(const String &path)
439 return !!PathIsDirectory(path.c_str());
442 //////////////////////////////////////////////////////////////////
443 // use IShellLink to expand the shortcut
444 // returns the expanded file, or "" on error
446 // original code was part of CShortcut
447 // 1996 by Rob Warner
448 // rhwarner@southeast.net
449 // http://users.southeast.net/~rhwarner
452 * @brief Expand given shortcut to full path.
453 * @param [in] inFile Shortcut to expand.
454 * @return Full path or empty string if error happened.
456 String ExpandShortcut(const String &inFile)
458 assert(inFile != _T(""));
460 // No path, nothing to return
461 if (inFile == _T(""))
468 // Create instance for shell link
469 hres = ::CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
470 IID_IShellLink, (LPVOID*) &psl);
473 // Get a pointer to the persist file interface
475 hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*) &ppf);
480 _tcscpy_safe(wsz, inFile.c_str());
482 ::MultiByteToWideChar(CP_ACP, 0, inFile.c_str(), -1, wsz, MAX_PATH);
486 hres = ppf->Load(wsz, STGM_READ);
490 // find the path from that
491 TCHAR buf[MAX_PATH] = {0};
492 psl->GetPath(buf, MAX_PATH, NULL, SLGP_UNCPRIORITY);
500 // if this fails, outFile == ""
505 * @brief Append subpath to path.
506 * This function appends subpath to given path. Function ensures there
507 * is only one backslash between path parts.
508 * @param [in] path "Base" path where other part is appended.
509 * @param [in] subpath Path part to append to base part.
510 * @return Formatted path. If one of arguments is empty then returns
511 * non-empty argument. If both argumets are empty empty string is returned.
513 String ConcatPath(const String & path, const String & subpath)
519 if (EndsWithSlash(path))
521 return String(path).append(subpath.c_str() + (IsSlash(subpath, 0) ? 1 : 0));
525 if (IsSlash(subpath, 0))
527 return path + subpath;
531 return path + _T("\\") + subpath;
537 * @brief Get parent path.
538 * This function returns parent path for given path. For example for
539 * path "c:\folder\subfolder" we return "c:\folder".
540 * @param [in] path Path to get parent path for.
541 * @return Parent path.
543 String GetParentPath(const String& path)
545 String parentPath(path);
546 size_t len = parentPath.length();
548 // Remove last '\' from paths
549 if (parentPath[len - 1] == '\\')
551 parentPath.resize(len - 1);
555 // Remove last part of path
556 size_t pos = parentPath.rfind('\\');
558 if (pos != parentPath.npos)
560 // Do not remove trailing slash from root directories
561 parentPath.resize(pos == 2 ? pos + 1 : pos);
567 * @brief Get last subdirectory of path.
569 * Returns last subdirectory name (if one exists) from given path.
571 * - C:\work\myproject returns \myproject
572 * @param [in] path Original path.
573 * @return Last subdirectory in path.
575 String GetLastSubdir(const String & path)
577 String parentPath(path);
578 size_t len = parentPath.length();
580 // Remove last '\' from paths
581 if (parentPath[len - 1] == '\\')
583 parentPath.erase(len - 1, 1);
587 // Find last part of path
588 size_t pos = parentPath.find_last_of('\\');
589 if (pos >= 2 && pos != String::npos)
590 parentPath.erase(0, pos);
595 * @brief Checks if path is an absolute path.
596 * @param [in] path Path to check.
597 * @return true if given path is absolute path.
599 bool IsPathAbsolute(const String &path)
601 if (path.length() < 3)
604 size_t pos = path.find_last_of('\\');
606 // Absolute path must have "\" and cannot start with it.
607 // Also "\\blahblah" is invalid.
608 if (pos < 2 || pos == String::npos)
611 // Maybe "X:\blahblah"?
612 if (path[1] == ':' && path[2] == '\\')
616 if (path[0] == '\\' && path[1] == '\\' && pos > 2)
623 * @brief Checks if folder exists and creates it if needed.
624 * This function checks if folder exists and creates it if not.
626 * @return Path if it exists or were created successfully. If
627 * path points to file or folder failed to create returns empty
630 String EnsurePathExist(const String & sPath)
632 int rtn = DoesPathExist(sPath);
633 if (rtn == IS_EXISTING_DIR)
635 if (rtn == IS_EXISTING_FILE)
637 if (!CreateIfNeeded(sPath))
639 // Check creating folder succeeded
640 if (DoesPathExist(sPath) == IS_EXISTING_DIR)
647 * @brief Return true if *pszChar is a slash (either direction) or a colon
649 * begin points to start of string, in case multibyte trail test is needed
651 bool IsSlashOrColon(const TCHAR *pszChar, const TCHAR *begin)
654 return (*pszChar == '/' || *pszChar == ':' || *pszChar == '\\');
656 // Avoid 0x5C (ASCII backslash) byte occurring as trail byte in MBCS
657 return (*pszChar == '/' || *pszChar == ':'
658 || (*pszChar == '\\' && !_ismbstrail((unsigned char *)begin, (unsigned char *)pszChar)));
663 * @brief Extract path name components from given full path.
664 * @param [in] pathLeft Original path.
665 * @param [out] pPath Folder name component of full path, excluding
667 * @param [out] pFile File name part, excluding extension.
668 * @param [out] pExt Filename extension part, excluding leading dot.
670 void SplitFilename(const String& pathLeft, String* pPath, String* pFile, String* pExt)
672 const TCHAR *pszChar = pathLeft.c_str() + pathLeft.length();
673 const TCHAR *pend = pszChar;
674 const TCHAR *extptr = 0;
677 while (pathLeft.c_str() < --pszChar)
685 (*pExt) = pszChar + 1;
687 ext = true; // extension is only after last period
691 else if (IsSlashOrColon(pszChar, pathLeft.c_str()))
693 // Ok, found last slash, so we collect any info desired
698 // Grab directory (omit trailing slash)
699 size_t len = pszChar - pathLeft.c_str();
701 ++len; // Keep trailing colon ( eg, C:filename.txt)
703 pPath->erase(len); // Cut rest of path
709 *pFile = pszChar + 1;
716 // Never found a delimiter
723 // if both filename & extension requested, remove extension from filename
725 if (pFile && pExt && extptr)
727 size_t extlen = pend - extptr;
728 pFile->erase(pFile->length() - extlen);
732 // Split Rational ClearCase view name (file_name@@file_version).
733 void SplitViewName(const TCHAR *s, String * path, String * name, String * ext)
736 size_t nOffset = sViewName.find(_T("@@"));
737 if (nOffset != String::npos)
739 sViewName.erase(nOffset);
740 SplitFilename(sViewName, path, name, ext);
745 * @brief Return path component from full path.
746 * @param [in] fullpath Full path to split.
747 * @return Path without filename.
749 String GetPathOnly(const String& fullpath)
751 if (fullpath.empty()) return _T("");
753 SplitFilename(fullpath, &spath, 0, 0);
757 bool IsURLorCLSID(const String& path)
759 return (path.find(_T("://")) != String::npos || path.find(_T("::{")) != String::npos);
762 bool IsDecendant(const String& path, const String& ancestor)
764 return path.length() > ancestor.length() &&
765 strutils::compare_nocase(String(path.c_str(), path.c_str() + ancestor.length()), ancestor) == 0;
768 static void replace_char(TCHAR *s, int target, int repl)
771 for (p=s; *p != _T('\0'); p = _tcsinc(p))
776 String ToWindowsPath(const String& path)
778 String winpath = path;
779 replace_char(&*winpath.begin(), '/', '\\');
783 String ToUnixPath(const String& path)
785 String unixpath = path;
786 replace_char(&*unixpath.begin(), '\\', '/');