OSDN Git Service

Reduce compilation time (2)
[winmerge-jp/winmerge-jp.git] / Src / paths.cpp
1 /** 
2  * @file  paths.cpp
3  *
4  * @brief Path handling routines
5  */
6
7 #include "pch.h"
8 #include "paths.h"
9 #include <windows.h>
10 #include <cassert>
11 #include <cstring>
12 #include <direct.h>
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.
15 #include <shlobj.h>
16 #pragma warning (pop)
17 #include <shlwapi.h>
18 #include "PathContext.h"
19 #include "coretools.h"
20 #include "TFile.h"
21
22 namespace paths
23 {
24
25 static bool IsSlash(const String& pszStart, size_t nPos);
26 static bool GetDirName(const String& sDir, String& sName);
27
28 /** 
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.
33  */
34 static bool IsSlash(const String& pszStart, size_t nPos)
35 {
36         return pszStart[nPos]=='/' || 
37 #ifdef _UNICODE
38                pszStart[nPos]=='\\';
39 #else
40                 // Avoid 0x5C (ASCII backslash) byte occurring as trail byte in MBCS
41                (pszStart[nPos]=='\\' && !_ismbstrail((unsigned char *)pszStart.c_str(), (unsigned char *)pszStart.c_str() + nPos));
42 #endif
43 }
44
45 /** 
46  * @brief Checks if string ends with slash.
47  * This function checks if given string ends with slash. In many places,
48  * especially in GUI, we assume folder paths end with slash.
49  * @param [in] s String to check.
50  * @return true if last char in string is slash.
51  */
52 bool EndsWithSlash(const String& s)
53 {
54         if (size_t len = s.length())
55                 return IsSlash(s, (int)len - 1);
56         return false;
57 }
58
59 /** 
60  * @brief Checks if path exists and if it points to folder or file.
61  * This function first checks if path exists. If path exists
62  * then function checks if path points to folder or file.
63  * @param [in] szPath Path to check.
64  * @return One of:
65  * - DOES_NOT_EXIST : path does not exists
66  * - IS_EXISTING_DIR : path points to existing folder
67  * - IS_EXISTING_FILE : path points to existing file
68  */
69 PATH_EXISTENCE DoesPathExist(const String& szPath, bool (*IsArchiveFile)(const String&) /*= nullptr*/)
70 {
71         if (szPath.empty())
72                 return DOES_NOT_EXIST;
73
74         // Expand environment variables:
75         // Convert "%userprofile%\My Documents" to "C:\Documents and Settings\username\My Documents"
76         const TCHAR *lpcszPath = szPath.c_str();
77         TCHAR expandedPath[MAX_PATH_FULL];
78
79         if (_tcschr(lpcszPath, '%') != nullptr)
80         {
81                 DWORD dwLen = ExpandEnvironmentStrings(lpcszPath, expandedPath, MAX_PATH_FULL);
82                 if (dwLen > 0 && dwLen < MAX_PATH_FULL)
83                         lpcszPath = expandedPath;
84         }
85
86         DWORD attr = GetFileAttributes(TFile(String(lpcszPath)).wpath().c_str());
87
88         if (attr == ((DWORD) -1))
89         {
90                 if (IsArchiveFile && IsArchiveFile(szPath))
91                         return IS_EXISTING_DIR;
92                 return DOES_NOT_EXIST;
93         }
94         else if (attr & FILE_ATTRIBUTE_DIRECTORY)
95                 return IS_EXISTING_DIR;
96         else
97         {
98                 if (IsArchiveFile && IsArchiveFile(szPath))
99                         return IS_EXISTING_DIR;
100                 return IS_EXISTING_FILE;
101         }
102 }
103
104 /**
105  * @brief Like shlwapi's PathFindFileName(), but works with both \ and /.
106  * @param [in] Path
107  * @return Filename
108  */
109 String FindFileName(const String& path)
110 {
111         const TCHAR *filename = path.c_str();
112         while (const TCHAR *slash = _tcspbrk(filename, _T("\\/")))
113         {
114                 if (*(slash + 1) == '\0')
115                         break;
116                 filename = slash + 1;
117         }
118         return filename;
119 }
120
121 /**
122  * @brief Like shlwapi's PathFindFileName(), but works with both \ and /.
123  * @param [in] Path
124  * @return Filename
125  */
126 String FindExtension(const String& path)
127 {
128         return ::PathFindExtension(path.c_str());
129 }
130
131 /** 
132  * @brief Strip trailing slas.
133  * This function strips trailing slash from given path. Root paths are special
134  * case and they are left intact. Since C:\ is a valid path but C: is not.
135  * @param [in,out] sPath Path to strip.
136  */
137 void normalize(String & sPath)
138 {
139         size_t len = sPath.length();
140         if (!len)
141                 return;
142
143         // prefix root with current drive
144         sPath = GetLongPath(sPath);
145
146         // Do not remove trailing slash from root directories
147         if (len == 3 && sPath[1] == ':')
148                 return;
149
150         // remove any trailing slash
151         if (EndsWithSlash(sPath))
152                 sPath.resize(sPath.length() - 1);
153 }
154
155 /**
156  * @brief Get canonical name of folder.
157  * @param [in] sDir Folder to handle.
158  * @param [out] sName Canonicalized folder name.
159  * @return true if canonical name exists.
160  * @todo Should we return empty string as sName when returning false?
161  */
162 static bool GetDirName(const String& sDir, String& sName)
163 {
164         // FindFirstFile doesn't work for root:
165         // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/findfirstfile.asp
166         // 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         {
169                 // I don't know if this work for empty root directories
170                 // because my first return value is not a dot directory, as I would have expected
171                 WIN32_FIND_DATA ffd;
172                 TCHAR sPath[8];
173                 StringCchPrintf(sPath, sizeof(sPath)/sizeof(sPath[0]), _T("%s\\*"), sDir.c_str());
174                 HANDLE h = FindFirstFile(sPath, &ffd);
175                 if (h == INVALID_HANDLE_VALUE)
176                         return false;
177                 FindClose(h);
178                 sName = sDir;
179                 return true;
180         }
181         // (Couldn't get info for just the directory from CFindFile)
182         WIN32_FIND_DATA ffd;
183         
184         HANDLE h = FindFirstFile(TFile(sDir).wpath().c_str(), &ffd);
185         if (h == INVALID_HANDLE_VALUE)
186                 return false;
187         sName = ffd.cFileName;
188         FindClose(h);
189         return true;
190 }
191
192 /**
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.
202  */
203 String GetLongPath(const String& szPath, bool bExpandEnvs)
204 {
205         String sPath = szPath;
206         size_t len = sPath.length();
207         if (len < 1)
208                 return sPath;
209
210         TCHAR fullPath[MAX_PATH_FULL] = {0};
211         TCHAR *pFullPath = &fullPath[0];
212         TCHAR *lpPart;
213
214         //                                         GetFullPathName  GetLongPathName
215         // Convert to fully qualified form              Yes               No
216         //    (Including .)
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
221         //
222         // Fully qualify/normalize name using GetFullPathName.
223
224         // Expand environment variables:
225         // Convert "%userprofile%\My Documents" to "C:\Documents and Settings\username\My Documents"
226         TCHAR expandedPath[MAX_PATH_FULL];
227         const TCHAR *lpcszPath = sPath.c_str();
228         if (bExpandEnvs && _tcschr(lpcszPath, '%') != nullptr)
229         {
230                 DWORD dwLen = ExpandEnvironmentStrings(lpcszPath, expandedPath, MAX_PATH_FULL);
231                 if (dwLen > 0 && dwLen < MAX_PATH_FULL)
232                         lpcszPath = expandedPath;
233         }
234         
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                 _tcscpy_s(pFullPath, MAX_PATH_FULL, tPath.c_str());
239
240         // We are done if this is not a short name.
241         if (_tcschr(pFullPath, _T('~')) == nullptr)
242                 return pFullPath;
243
244         // We have to do it the hard way because GetLongPathName is not
245         // available on Win9x and some WinNT 4
246
247         // The file/directory does not exist, use as much long name as we can
248         // and leave the invalid stuff at the end.
249         String sLong;
250         TCHAR *ptr = pFullPath;
251         TCHAR *end = nullptr;
252
253         // Skip to \ position     d:\abcd or \\host\share\abcd
254         // indicated by ^           ^                    ^
255         if (_tcslen(ptr) > 2)
256                 end = _tcschr(pFullPath+2, _T('\\'));
257         if (end != nullptr && !_tcsncmp(pFullPath, _T("\\\\"),2))
258                 end = _tcschr(end+1, _T('\\'));
259
260         if (end == nullptr)
261                 return pFullPath;
262
263         *end = 0;
264         sLong += ptr;
265         ptr = &end[1];
266
267         // now walk down each directory and do short to long name conversion
268         while (ptr != nullptr)
269         {
270                 end = _tcschr(ptr, '\\');
271                 // zero-terminate current component
272                 // (if we're at end, its already zero-terminated)
273                 if (end != nullptr)
274                         *end = 0;
275
276                 String sTemp(sLong);
277                 sTemp += '\\';
278                 sTemp += ptr;
279
280                 // advance to next component (or set ptr=`nullptr` to flag end)
281                 ptr = (end!=nullptr ? end+1 : nullptr);
282
283                 // (Couldn't get info for just the directory from CFindFile)
284                 WIN32_FIND_DATA ffd;
285                 HANDLE h = FindFirstFile(TFile(sTemp).wpath().c_str(), &ffd);
286                 if (h == INVALID_HANDLE_VALUE)
287                 {
288                         sLong = sTemp;
289                         if (ptr != nullptr)
290                         {
291                                 sLong += '\\';
292                                 sLong += ptr;
293                         }
294                         return sLong;
295                 }
296                 sLong += '\\';
297                 sLong += ffd.cFileName;
298                 FindClose(h);
299         }
300         return sLong;
301 }
302
303 /**
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.
311  */
312 bool CreateIfNeeded(const String& szPath)
313 {
314         if (szPath.empty())
315                 return false;
316
317         String sTemp;
318         if (GetDirName(szPath, sTemp))
319                 return true;
320
321         if (szPath.length() >= MAX_PATH_FULL)
322                 return false;
323
324         // Expand environment variables:
325         // Convert "%userprofile%\My Documents" to "C:\Documents and Settings\username\My Documents"
326         TCHAR fullPath[MAX_PATH_FULL];
327         fullPath[0] = '\0';
328         if (_tcschr(szPath.c_str(), '%') != nullptr)
329         {
330                 DWORD dwLen = ExpandEnvironmentStrings(szPath.c_str(), fullPath, MAX_PATH_FULL);
331                 if (dwLen == 0 || dwLen >= MAX_PATH_FULL)
332                         _tcscpy_safe(fullPath, szPath.c_str());
333         }
334         else
335                 _tcscpy_safe(fullPath, szPath.c_str());
336         // Now fullPath holds our desired path
337
338         TCHAR *ptr = fullPath;
339         TCHAR *end = nullptr;
340
341         // Skip to \ position     d:\abcd or \\host\share\abcd
342         // indicated by ^           ^                    ^
343         if (_tcslen(ptr) > 2)
344                 end = _tcschr(fullPath+2, _T('\\'));
345         if (end != nullptr && !_tcsncmp(fullPath, _T("\\\\"),2))
346                 end = _tcschr(end+1, _T('\\'));
347
348         if (end == nullptr) return false;
349
350         // check that first component exists
351         *end = 0;
352         if (!GetDirName(fullPath, sTemp))
353                 return false;
354         *end = '\\';
355
356         ptr = end+1;
357
358         while (ptr != nullptr)
359         {
360                 end = _tcschr(ptr, '\\');
361                 // zero-terminate current component
362                 // (if we're at end, its already zero-terminated)
363                 if (end != nullptr)
364                         *end = 0;
365
366                 // advance to next component (or set ptr=`nullptr` to flag end)
367                 ptr = (end != nullptr ? end+1 : nullptr);
368
369                 String sNextName;
370                 if (!GetDirName(fullPath, sNextName))
371                 {
372                         // try to create directory, and then double-check its existence
373                         if (!CreateDirectory(fullPath, 0) ||
374                                 !GetDirName(fullPath, sNextName))
375                         {
376                                 return false;
377                         }
378                 }
379                 // if not finished, restore directory string we're working in
380                 if (ptr != nullptr)
381                         *end = '\\';
382         }
383         return true;
384 }
385
386 /** 
387  * @brief Check if paths are both folders or files.
388  * This function checks if paths are "compatible" as in many places we need
389  * to have two folders or two files.
390  * @param [in] paths Left and right paths.
391  * @return One of:
392  *  - IS_EXISTING_DIR : both are directories & exist
393  *  - IS_EXISTING_FILE : both are files & exist
394  *  - DOES_NOT_EXIST : in all other cases
395 */
396 PATH_EXISTENCE GetPairComparability(const PathContext & paths, bool (*IsArchiveFile)(const String&) /*= nullptr*/)
397 {
398         // fail if not both specified
399         if (paths.GetSize() < 2 || paths[0].empty() || paths[1].empty())
400                 return DOES_NOT_EXIST;
401         PATH_EXISTENCE p1 = DoesPathExist(paths[0], IsArchiveFile);
402         // short circuit testing right if left doesn't exist
403         if (p1 == DOES_NOT_EXIST)
404                 return DOES_NOT_EXIST;
405         PATH_EXISTENCE p2 = DoesPathExist(paths[1], IsArchiveFile);
406         if (p1 != p2)
407         {
408                 p1 = DoesPathExist(paths[0]);
409                 p2 = DoesPathExist(paths[1]);
410                 if (p1 != p2)
411                         return DOES_NOT_EXIST;
412         }
413         if (paths.GetSize() < 3) return p1; 
414         PATH_EXISTENCE p3 = DoesPathExist(paths[2], IsArchiveFile);
415         if (p2 != p3)
416         {
417                 p1 = DoesPathExist(paths[0]);
418                 p2 = DoesPathExist(paths[1]);
419                 p3 = DoesPathExist(paths[2]);
420                 if (p1 != p2 || p2 != p3)
421                         return DOES_NOT_EXIST;
422         }
423         return p1;
424 }
425
426 /**
427  * @brief Check if the given path points to shotcut.
428  * Windows considers paths having a filename with extension ".lnk" as
429  * shortcuts. This function checks if the given path is shortcut.
430  * We usually want to expand shortcuts with ExpandShortcut().
431  * @param [in] inPath Path to check;
432  * @return true if the path points to shortcut, false otherwise.
433  */
434 bool IsShortcut(const String& inPath)
435 {
436         const TCHAR ShortcutExt[] = _T(".lnk");
437         TCHAR ext[_MAX_EXT] = {0};
438         _tsplitpath_s(inPath.c_str(), nullptr, 0, nullptr, 0, nullptr, 0, ext, _MAX_EXT);
439         if (_tcsicmp(ext, ShortcutExt) == 0)
440                 return true;
441         else
442                 return false;
443 }
444
445 bool IsDirectory(const String &path)
446 {
447         return !!PathIsDirectory(path.c_str());
448 }
449
450 //////////////////////////////////////////////////////////////////
451 //      use IShellLink to expand the shortcut
452 //      returns the expanded file, or "" on error
453 //
454 //      original code was part of CShortcut 
455 //      1996 by Rob Warner
456 //      rhwarner@southeast.net
457 //      http://users.southeast.net/~rhwarner
458
459 /** 
460  * @brief Expand given shortcut to full path.
461  * @param [in] inFile Shortcut to expand.
462  * @return Full path or empty string if error happened.
463  */
464 String ExpandShortcut(const String &inFile)
465 {
466         assert(inFile != _T(""));
467
468         // No path, nothing to return
469         if (inFile == _T(""))
470                 return _T("");
471
472         String outFile;
473         IShellLink* psl;
474         HRESULT hres;
475
476         // Create instance for shell link
477         hres = ::CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
478                 IID_IShellLink, (LPVOID*) &psl);
479         if (SUCCEEDED(hres))
480         {
481                 // Get a pointer to the persist file interface
482                 IPersistFile* ppf;
483                 hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*) &ppf);
484                 if (SUCCEEDED(hres))
485                 {
486                         WCHAR wsz[MAX_PATH_FULL];
487 #ifdef _UNICODE
488                         _tcscpy_safe(wsz, inFile.c_str());
489 #else
490                         ::MultiByteToWideChar(CP_ACP, 0, inFile.c_str(), -1, wsz, MAX_PATH_FULL);
491 #endif
492
493                         // Load shortcut
494                         hres = ppf->Load(wsz, STGM_READ);
495
496                         if (SUCCEEDED(hres))
497                         {
498                                 // find the path from that
499                                 TCHAR buf[MAX_PATH_FULL] = {0};
500                                 psl->GetPath(buf, MAX_PATH_FULL, nullptr, SLGP_UNCPRIORITY);
501                                 outFile = buf;
502                         }
503                         ppf->Release();
504                 }
505                 psl->Release();
506         }
507
508         // if this fails, outFile == ""
509         return outFile;
510 }
511
512 /** 
513  * @brief Append subpath to path.
514  * This function appends subpath to given path. Function ensures there
515  * is only one backslash between path parts.
516  * @param [in] path "Base" path where other part is appended.
517  * @param [in] subpath Path part to append to base part.
518  * @return Formatted path. If one of arguments is empty then returns
519  * non-empty argument. If both argumets are empty empty string is returned.
520  */
521 String ConcatPath(const String & path, const String & subpath)
522 {
523         if (path.empty())
524                 return subpath;
525         if (subpath.empty())
526                 return path;
527         if (EndsWithSlash(path))
528         {
529                 return String(path).append(subpath.c_str() + (IsSlash(subpath, 0) ? 1 : 0));
530         }
531         else
532         {
533                 if (IsSlash(subpath, 0))
534                 {
535                         return path + subpath;
536                 }
537                 else
538                 {
539                         return path + _T("\\") + subpath;
540                 }
541         }
542 }
543
544 /** 
545  * @brief Get parent path.
546  * This function returns parent path for given path. For example for
547  * path "c:\folder\subfolder" we return "c:\folder".
548  * @param [in] path Path to get parent path for.
549  * @return Parent path.
550  */
551 String GetParentPath(const String& path)
552 {
553         String parentPath(path);
554         size_t len = parentPath.length();
555
556         // Remove last '\' from paths
557         if (parentPath[len - 1] == '\\')
558         {
559                 parentPath.resize(len - 1);
560                 --len;
561         }
562
563         // Remove last part of path
564         size_t pos = parentPath.rfind('\\');
565
566         if (pos != parentPath.npos)
567         {
568                 // Do not remove trailing slash from root directories
569                 parentPath.resize(pos == 2 ? pos + 1 : pos);
570         }
571         return parentPath;
572 }
573
574 /** 
575  * @brief Get last subdirectory of path.
576  *
577  * Returns last subdirectory name (if one exists) from given path.
578  * For example:
579  * - C:\work\myproject returns \myproject
580  * @param [in] path Original path.
581  * @return Last subdirectory in path.
582  */
583 String GetLastSubdir(const String & path)
584 {
585         String parentPath(path);
586         size_t len = parentPath.length();
587
588         // Remove last '\' from paths
589         if (parentPath[len - 1] == '\\')
590         {
591                 parentPath.erase(len - 1, 1);
592                 --len;
593         }
594
595         // Find last part of path
596         size_t pos = parentPath.find_last_of('\\');
597         if (pos >= 2 && pos != String::npos)
598                 parentPath.erase(0, pos);
599         return parentPath;
600 }
601
602 /** 
603  * @brief Checks if path is an absolute path.
604  * @param [in] path Path to check.
605  * @return true if given path is absolute path.
606  */
607 bool IsPathAbsolute(const String &path)
608 {
609         if (path.length() < 3)
610                 return false;
611         
612         size_t pos = path.find_last_of('\\');
613
614         // Absolute path must have "\" and cannot start with it.
615         // Also "\\blahblah" is invalid.
616         if (pos < 2 || pos == String::npos)
617                 return false;
618
619         // Maybe "X:\blahblah"?
620         if (path[1] == ':' && path[2] == '\\')
621                 return true;
622
623         // So "\\blahblah\"?
624         if (path[0] == '\\' && path[1] == '\\' && pos > 2)
625                 return true;
626         else
627                 return false;
628 }
629
630 /**
631  * @brief Checks if folder exists and creates it if needed.
632  * This function checks if folder exists and creates it if not.
633  * @param [in] sPath
634  * @return Path if it exists or were created successfully. If
635  * path points to file or folder failed to create returns empty
636  * string.
637  */
638 String EnsurePathExist(const String & sPath)
639 {
640         int rtn = DoesPathExist(sPath);
641         if (rtn == IS_EXISTING_DIR)
642                 return sPath;
643         if (rtn == IS_EXISTING_FILE)
644                 return _T("");
645         if (!CreateIfNeeded(sPath))
646                 return _T("");
647         // Check creating folder succeeded
648         if (DoesPathExist(sPath) == IS_EXISTING_DIR)
649                 return sPath;
650         else
651                 return _T("");
652 }
653
654 /**
655  * @brief Return true if *pszChar is a slash (either direction) or a colon
656  *
657  * begin points to start of string, in case multibyte trail test is needed
658  */
659 bool IsSlashOrColon(const TCHAR *pszChar, const TCHAR *begin)
660 {
661 #ifdef _UNICODE
662                 return (*pszChar == '/' || *pszChar == ':' || *pszChar == '\\');
663 #else
664                 // Avoid 0x5C (ASCII backslash) byte occurring as trail byte in MBCS
665                 return (*pszChar == '/' || *pszChar == ':' 
666                         || (*pszChar == '\\' && !_ismbstrail((unsigned char *)begin, (unsigned char *)pszChar)));
667 #endif
668 }
669
670 /**
671  * @brief Extract path name components from given full path.
672  * @param [in] pathLeft Original path.
673  * @param [out] pPath Folder name component of full path, excluding
674    trailing slash.
675  * @param [out] pFile File name part, excluding extension.
676  * @param [out] pExt Filename extension part, excluding leading dot.
677  */
678 void SplitFilename(const String& pathLeft, String* pPath, String* pFile, String* pExt)
679 {
680         const TCHAR *pszChar = pathLeft.c_str() + pathLeft.length();
681         const TCHAR *pend = pszChar;
682         const TCHAR *extptr = 0;
683         bool ext = false;
684
685         while (pathLeft.c_str() < --pszChar)
686         {
687                 if (*pszChar == '.')
688                 {
689                         if (!ext)
690                         {
691                                 if (pExt != nullptr)
692                                 {
693                                         (*pExt) = pszChar + 1;
694                                 }
695                                 ext = true; // extension is only after last period
696                                 extptr = pszChar;
697                         }
698                 }
699                 else if (IsSlashOrColon(pszChar, pathLeft.c_str()))
700                 {
701                         // Ok, found last slash, so we collect any info desired
702                         // and we're done
703
704                         if (pPath != nullptr)
705                         {
706                                 // Grab directory (omit trailing slash)
707                                 size_t len = pszChar - pathLeft.c_str();
708                                 if (*pszChar == ':')
709                                         ++len; // Keep trailing colon ( eg, C:filename.txt)
710                                 *pPath = pathLeft;
711                                 pPath->erase(len); // Cut rest of path
712                         }
713
714                         if (pFile != nullptr)
715                         {
716                                 // Grab file
717                                 *pFile = pszChar + 1;
718                         }
719
720                         goto endSplit;
721                 }
722         }
723
724         // Never found a delimiter
725         if (pFile != nullptr)
726         {
727                 *pFile = pathLeft;
728         }
729
730 endSplit:
731         // if both filename & extension requested, remove extension from filename
732
733         if (pFile != nullptr && pExt != nullptr && extptr != nullptr)
734         {
735                 size_t extlen = pend - extptr;
736                 pFile->erase(pFile->length() - extlen);
737         }
738 }
739
740 /**
741  * @brief Return path component from full path.
742  * @param [in] fullpath Full path to split.
743  * @return Path without filename.
744  */
745 String GetPathOnly(const String& fullpath)
746 {
747         if (fullpath.empty()) return _T("");
748         String spath;
749         SplitFilename(fullpath, &spath, 0, 0);
750         return spath;
751 }
752
753 bool IsURLorCLSID(const String& path)
754 {
755         return (path.find(_T("://")) != String::npos || path.find(_T("::{")) != String::npos);
756 }
757
758 bool IsDecendant(const String& path, const String& ancestor)
759 {
760         return path.length() > ancestor.length() && 
761                    strutils::compare_nocase(String(path.c_str(), path.c_str() + ancestor.length()), ancestor) == 0;
762 }
763
764 static void replace_char(TCHAR *s, int target, int repl)
765 {
766         TCHAR *p;
767         for (p=s; *p != _T('\0'); p = _tcsinc(p))
768                 if (*p == target)
769                         *p = (TCHAR)repl;
770 }
771
772 String ToWindowsPath(const String& path)
773 {
774         String winpath = path;
775         replace_char(&*winpath.begin(), '/', '\\');
776         return winpath;
777 }
778
779 String ToUnixPath(const String& path)
780 {
781         String unixpath = path;
782         replace_char(&*unixpath.begin(), '\\', '/');
783         return unixpath;
784 }
785
786 }