OSDN Git Service

An attempt to reduce build time
[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 IsDirName(const String& sDir);
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_t *lpcszPath = szPath.c_str();
72         tchar_t expandedPath[MAX_PATH_FULL];
73
74         if (tc::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_t *filename = path.c_str();
107         while (const tchar_t *slash = tc::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 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
160  */
161 static bool IsDirName(const String& sDir)
162 {
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.
166         size_t count = 0;
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() == '\\'))))
172         {
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
175                 WIN32_FIND_DATA ffd;
176                 HANDLE h = FindFirstFile((sDir + (sDir.back() == '\\' ? _T("*") : _T("\\*"))).c_str(), &ffd);
177                 if (h == INVALID_HANDLE_VALUE)
178                         return false;
179                 FindClose(h);
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         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_t fullPath[MAX_PATH_FULL] = {0};
211         tchar_t *pFullPath = &fullPath[0];
212         tchar_t *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_t expandedPath[MAX_PATH_FULL];
227         const tchar_t *lpcszPath = sPath.c_str();
228         if (bExpandEnvs && tc::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                 tc::tcslcpy(pFullPath, MAX_PATH_FULL, tPath.c_str());
239
240         // We are done if this is not a short name.
241         if (tc::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_t *ptr = pFullPath;
251         tchar_t *end = nullptr;
252
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('\\'));
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 = tc::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 = std::move(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         if (IsDirName(szPath))
318                 return true;
319
320         if (szPath.length() >= MAX_PATH_FULL)
321                 return false;
322
323         // Expand environment variables:
324         // Convert "%userprofile%\My Documents" to "C:\Documents and Settings\username\My Documents"
325         tchar_t fullPath[MAX_PATH_FULL];
326         fullPath[0] = '\0';
327         if (tc::tcschr(szPath.c_str(), '%') != nullptr)
328         {
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());
332         }
333         else
334                 tc::tcslcpy(fullPath, szPath.c_str());
335         // Now fullPath holds our desired path
336
337         tchar_t *ptr = fullPath;
338         tchar_t *end = nullptr;
339
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('\\'));
346
347         if (end == nullptr) return false;
348
349         // check that first component exists
350         *end = 0;
351         if (!IsDirName(fullPath))
352                 return false;
353         *end = '\\';
354
355         ptr = end+1;
356
357         while (ptr != nullptr)
358         {
359                 end = tc::tcschr(ptr, '\\');
360                 // zero-terminate current component
361                 // (if we're at end, its already zero-terminated)
362                 if (end != nullptr)
363                         *end = 0;
364
365                 // advance to next component (or set ptr=`nullptr` to flag end)
366                 ptr = (end != nullptr ? end+1 : nullptr);
367
368                 if (!IsDirName(fullPath))
369                 {
370                         // try to create directory, and then double-check its existence
371                         if (!CreateDirectory(fullPath, 0) ||
372                                 !IsDirName(fullPath))
373                         {
374                                 return false;
375                         }
376                 }
377                 // if not finished, restore directory string we're working in
378                 if (ptr != nullptr)
379                         *end = '\\';
380         }
381         return true;
382 }
383
384 /** 
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.
389  * @return One of:
390  *  - IS_EXISTING_DIR : both are directories & exist
391  *  - IS_EXISTING_FILE : both are files & exist
392  *  - DOES_NOT_EXIST : in all other cases
393 */
394 PATH_EXISTENCE GetPairComparability(const PathContext & paths, bool (*IsArchiveFile)(const String&) /*= nullptr*/)
395 {
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);
404         if (p1 != p2)
405         {
406                 p1 = DoesPathExist(paths[0]);
407                 p2 = DoesPathExist(paths[1]);
408                 if (p1 != p2)
409                         return DOES_NOT_EXIST;
410         }
411         if (paths.GetSize() < 3) return p1; 
412         PATH_EXISTENCE p3 = DoesPathExist(paths[2], IsArchiveFile);
413         if (p2 != p3)
414         {
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;
420         }
421         return p1;
422 }
423
424 /**
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.
431  */
432 bool IsShortcut(const String& inPath)
433 {
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)
438                 return true;
439         else
440                 return false;
441 }
442
443 bool IsDirectory(const String &path)
444 {
445         return !!PathIsDirectory(path.c_str());
446 }
447
448 //////////////////////////////////////////////////////////////////
449 //      use IShellLink to expand the shortcut
450 //      returns the expanded file, or "" on error
451 //
452 //      original code was part of CShortcut 
453 //      1996 by Rob Warner
454 //      rhwarner@southeast.net
455 //      http://users.southeast.net/~rhwarner
456
457 /** 
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.
461  */
462 String ExpandShortcut(const String &inFile)
463 {
464         assert(!inFile.empty());
465
466         // No path, nothing to return
467         if (inFile.empty())
468                 return _T("");
469
470         String outFile;
471         IShellLink* psl;
472         HRESULT hres;
473
474         // Create instance for shell link
475         hres = ::CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
476                 IID_IShellLink, (LPVOID*) &psl);
477         if (SUCCEEDED(hres))
478         {
479                 // Get a pointer to the persist file interface
480                 IPersistFile* ppf;
481                 hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*) &ppf);
482                 if (SUCCEEDED(hres))
483                 {
484                         WCHAR wsz[MAX_PATH_FULL];
485                         tc::tcslcpy(wsz, inFile.c_str());
486
487                         // Load shortcut
488                         hres = ppf->Load(wsz, STGM_READ);
489
490                         if (SUCCEEDED(hres))
491                         {
492                                 // find the path from that
493                                 tchar_t buf[MAX_PATH_FULL] = {0};
494                                 psl->GetPath(buf, MAX_PATH_FULL, nullptr, SLGP_UNCPRIORITY);
495                                 outFile = buf;
496                         }
497                         ppf->Release();
498                 }
499                 psl->Release();
500         }
501
502         // if this fails, outFile == ""
503         return outFile;
504 }
505
506 /** 
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.
514  */
515 String ConcatPath(const String & path, const String & subpath)
516 {
517         if (path.empty())
518                 return subpath;
519         if (subpath.empty())
520                 return path;
521         if (EndsWithSlash(path))
522         {
523                 return String(path).append(subpath.c_str() + (IsSlash(subpath, 0) ? 1 : 0));
524         }
525         else
526         {
527                 if (IsSlash(subpath, 0))
528                 {
529                         return path + subpath;
530                 }
531                 else
532                 {
533                         return path + _T("\\") + subpath;
534                 }
535         }
536 }
537
538 /** 
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.
544  */
545 String GetParentPath(const String& path)
546 {
547         String parentPath(path);
548         size_t len = parentPath.length();
549
550         // Remove last '\' from paths
551         if (parentPath[len - 1] == '\\')
552         {
553                 parentPath.resize(len - 1);
554                 --len;
555         }
556
557         // Remove last part of path
558         size_t pos = parentPath.rfind('\\');
559
560         if (pos != parentPath.npos)
561         {
562                 // Do not remove trailing slash from root directories
563                 parentPath.resize(pos == 2 ? pos + 1 : pos);
564         }
565         return parentPath;
566 }
567
568 /** 
569  * @brief Get last subdirectory of path.
570  *
571  * Returns last subdirectory name (if one exists) from given path.
572  * For example:
573  * - C:\work\myproject returns \myproject
574  * @param [in] path Original path.
575  * @return Last subdirectory in path.
576  */
577 String GetLastSubdir(const String & path)
578 {
579         String parentPath(path);
580         size_t len = parentPath.length();
581
582         // Remove last '\' from paths
583         if (parentPath[len - 1] == '\\')
584         {
585                 parentPath.erase(len - 1, 1);
586                 --len;
587         }
588
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);
593         return parentPath;
594 }
595
596 /** 
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.
600  */
601 bool IsPathAbsolute(const String &path)
602 {
603         if (path.length() < 3)
604                 return false;
605         
606         size_t pos = path.find_last_of('\\');
607
608         // Absolute path must have "\" and cannot start with it.
609         // Also "\\blahblah" is invalid.
610         if (pos < 2 || pos == String::npos)
611                 return false;
612
613         // Maybe "X:\blahblah"?
614         if (path[1] == ':' && path[2] == '\\')
615                 return true;
616
617         // So "\\blahblah\"?
618         if (path[0] == '\\' && path[1] == '\\' && pos > 2)
619                 return true;
620         else
621                 return false;
622 }
623
624 /**
625  * @brief Checks if folder exists and creates it if needed.
626  * This function checks if folder exists and creates it if not.
627  * @param [in] sPath
628  * @return Path if it exists or were created successfully. If
629  * path points to file or folder failed to create returns empty
630  * string.
631  */
632 String EnsurePathExist(const String & sPath)
633 {
634         int rtn = DoesPathExist(sPath);
635         if (rtn == IS_EXISTING_DIR)
636                 return sPath;
637         if (rtn == IS_EXISTING_FILE)
638                 return _T("");
639         if (!CreateIfNeeded(sPath))
640                 return _T("");
641         // Check creating folder succeeded
642         if (DoesPathExist(sPath) == IS_EXISTING_DIR)
643                 return sPath;
644         else
645                 return _T("");
646 }
647
648 /**
649  * @brief Return true if *pszChar is a slash (either direction) or a colon
650  *
651  * begin points to start of string, in case multibyte trail test is needed
652  */
653 bool IsSlashOrColon(const tchar_t *pszChar, const tchar_t *begin)
654 {
655                 return (*pszChar == '/' || *pszChar == ':' || *pszChar == '\\');
656 }
657
658 /**
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
662    trailing slash.
663  * @param [out] pFile File name part, excluding extension.
664  * @param [out] pExt Filename extension part, excluding leading dot.
665  */
666 void SplitFilename(const String& pathLeft, String* pPath, String* pFile, String* pExt)
667 {
668         const tchar_t *pszChar = pathLeft.c_str() + pathLeft.length();
669         const tchar_t *pend = pszChar;
670         const tchar_t *extptr = 0;
671         bool ext = false;
672
673         while (pathLeft.c_str() < --pszChar)
674         {
675                 if (*pszChar == '.')
676                 {
677                         if (!ext)
678                         {
679                                 if (pExt != nullptr)
680                                 {
681                                         (*pExt) = pszChar + 1;
682                                 }
683                                 ext = true; // extension is only after last period
684                                 extptr = pszChar;
685                         }
686                 }
687                 else if (IsSlashOrColon(pszChar, pathLeft.c_str()))
688                 {
689                         // Ok, found last slash, so we collect any info desired
690                         // and we're done
691
692                         if (pPath != nullptr)
693                         {
694                                 // Grab directory (omit trailing slash)
695                                 size_t len = pszChar - pathLeft.c_str();
696                                 if (*pszChar == ':')
697                                         ++len; // Keep trailing colon ( eg, C:filename.txt)
698                                 *pPath = pathLeft;
699                                 pPath->erase(len); // Cut rest of path
700                         }
701
702                         if (pFile != nullptr)
703                         {
704                                 // Grab file
705                                 *pFile = pszChar + 1;
706                         }
707
708                         goto endSplit;
709                 }
710         }
711
712         // Never found a delimiter
713         if (pFile != nullptr)
714         {
715                 *pFile = pathLeft;
716         }
717
718 endSplit:
719         // if both filename & extension requested, remove extension from filename
720
721         if (pFile != nullptr && pExt != nullptr && extptr != nullptr)
722         {
723                 size_t extlen = pend - extptr;
724                 pFile->erase(pFile->length() - extlen);
725         }
726 }
727
728 /**
729  * @brief Return path component from full path.
730  * @param [in] fullpath Full path to split.
731  * @return Path without filename.
732  */
733 String GetPathOnly(const String& fullpath)
734 {
735         if (fullpath.empty()) return _T("");
736         String spath;
737         SplitFilename(fullpath, &spath, 0, 0);
738         return spath;
739 }
740
741 bool IsURL(const String& abspath)
742 {
743         for (size_t i = 0; i < abspath.length(); ++i)
744         {
745                 const auto c = abspath[i];
746                 if (c == '\\' || c == '/')
747                 {
748                         // If there is a \ or / before the : character, consider it not a URL.
749                         return false;
750                 }
751                 else if (c == ':')
752                         return (i != 1);
753         }
754         return false;
755 }
756
757 bool IsURLorCLSID(const String& path)
758 {
759         return IsURL(path) || path.find(_T("::{")) != String::npos;
760 }
761
762 bool isFileURL(const String& path)
763 {
764         return UrlIsFileUrl(path.c_str());
765 }
766
767 String FromURL(const String& url)
768 {
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);
772         return path.data();
773 }
774
775 bool IsDecendant(const String& path, const String& ancestor)
776 {
777         return path.length() > ancestor.length() && 
778                    strutils::compare_nocase(String(path.c_str(), path.c_str() + ancestor.length()), ancestor) == 0;
779 }
780
781 static void replace_char(tchar_t *s, int target, int repl)
782 {
783         tchar_t *p;
784         for (p=s; *p != _T('\0'); p = tc::tcsinc(p))
785                 if (*p == target)
786                         *p = (tchar_t)repl;
787 }
788
789 String ToWindowsPath(const String& path)
790 {
791         String winpath = path;
792         replace_char(&*winpath.begin(), '/', '\\');
793         return winpath;
794 }
795
796 String ToUnixPath(const String& path)
797 {
798         String unixpath = path;
799         replace_char(&*unixpath.begin(), '\\', '/');
800         return unixpath;
801 }
802
803 /**
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.
808  */
809 bool IsValidName(const String& name)
810 {
811         for (String::const_iterator it = name.begin(); it != name.end(); ++it)
812                 if (!(PathGetCharType(*it) & GCT_LFNCHAR))
813                         return false;
814
815         return true;
816 }
817
818 }