OSDN Git Service

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