Load files from subfolders in texturepacks
[oweals/minetest.git] / src / filesys.cpp
1 /*
2 Minetest
3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "filesys.h"
21 #include "util/string.h"
22 #include <iostream>
23 #include <cstdio>
24 #include <cstring>
25 #include <cerrno>
26 #include <fstream>
27 #include "log.h"
28 #include "config.h"
29 #include "porting.h"
30
31 namespace fs
32 {
33
34 #ifdef _WIN32 // WINDOWS
35
36 #define _WIN32_WINNT 0x0501
37 #include <windows.h>
38 #include <shlwapi.h>
39
40 std::vector<DirListNode> GetDirListing(const std::string &pathstring)
41 {
42         std::vector<DirListNode> listing;
43
44         WIN32_FIND_DATA FindFileData;
45         HANDLE hFind = INVALID_HANDLE_VALUE;
46         DWORD dwError;
47
48         std::string dirSpec = pathstring + "\\*";
49
50         // Find the first file in the directory.
51         hFind = FindFirstFile(dirSpec.c_str(), &FindFileData);
52
53         if (hFind == INVALID_HANDLE_VALUE) {
54                 dwError = GetLastError();
55                 if (dwError != ERROR_FILE_NOT_FOUND && dwError != ERROR_PATH_NOT_FOUND) {
56                         errorstream << "GetDirListing: FindFirstFile error."
57                                         << " Error is " << dwError << std::endl;
58                 }
59         } else {
60                 // NOTE:
61                 // Be very sure to not include '..' in the results, it will
62                 // result in an epic failure when deleting stuff.
63
64                 DirListNode node;
65                 node.name = FindFileData.cFileName;
66                 node.dir = FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
67                 if (node.name != "." && node.name != "..")
68                         listing.push_back(node);
69
70                 // List all the other files in the directory.
71                 while (FindNextFile(hFind, &FindFileData) != 0) {
72                         DirListNode node;
73                         node.name = FindFileData.cFileName;
74                         node.dir = FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
75                         if(node.name != "." && node.name != "..")
76                                 listing.push_back(node);
77                 }
78
79                 dwError = GetLastError();
80                 FindClose(hFind);
81                 if (dwError != ERROR_NO_MORE_FILES) {
82                         errorstream << "GetDirListing: FindNextFile error."
83                                         << " Error is " << dwError << std::endl;
84                         listing.clear();
85                         return listing;
86                 }
87         }
88         return listing;
89 }
90
91 bool CreateDir(const std::string &path)
92 {
93         bool r = CreateDirectory(path.c_str(), NULL);
94         if(r == true)
95                 return true;
96         if(GetLastError() == ERROR_ALREADY_EXISTS)
97                 return true;
98         return false;
99 }
100
101 bool PathExists(const std::string &path)
102 {
103         return (GetFileAttributes(path.c_str()) != INVALID_FILE_ATTRIBUTES);
104 }
105
106 bool IsPathAbsolute(const std::string &path)
107 {
108         return !PathIsRelative(path.c_str());
109 }
110
111 bool IsDir(const std::string &path)
112 {
113         DWORD attr = GetFileAttributes(path.c_str());
114         return (attr != INVALID_FILE_ATTRIBUTES &&
115                         (attr & FILE_ATTRIBUTE_DIRECTORY));
116 }
117
118 bool IsDirDelimiter(char c)
119 {
120         return c == '/' || c == '\\';
121 }
122
123 bool RecursiveDelete(const std::string &path)
124 {
125         infostream<<"Recursively deleting \""<<path<<"\""<<std::endl;
126
127         DWORD attr = GetFileAttributes(path.c_str());
128         bool is_directory = (attr != INVALID_FILE_ATTRIBUTES &&
129                         (attr & FILE_ATTRIBUTE_DIRECTORY));
130         if(!is_directory)
131         {
132                 infostream<<"RecursiveDelete: Deleting file "<<path<<std::endl;
133                 //bool did = DeleteFile(path.c_str());
134                 bool did = true;
135                 if(!did){
136                         errorstream<<"RecursiveDelete: Failed to delete file "
137                                         <<path<<std::endl;
138                         return false;
139                 }
140         }
141         else
142         {
143                 infostream<<"RecursiveDelete: Deleting content of directory "
144                                 <<path<<std::endl;
145                 std::vector<DirListNode> content = GetDirListing(path);
146                 for(size_t i=0; i<content.size(); i++){
147                         const DirListNode &n = content[i];
148                         std::string fullpath = path + DIR_DELIM + n.name;
149                         bool did = RecursiveDelete(fullpath);
150                         if(!did){
151                                 errorstream<<"RecursiveDelete: Failed to recurse to "
152                                                 <<fullpath<<std::endl;
153                                 return false;
154                         }
155                 }
156                 infostream<<"RecursiveDelete: Deleting directory "<<path<<std::endl;
157                 //bool did = RemoveDirectory(path.c_str();
158                 bool did = true;
159                 if(!did){
160                         errorstream<<"Failed to recursively delete directory "
161                                         <<path<<std::endl;
162                         return false;
163                 }
164         }
165         return true;
166 }
167
168 bool DeleteSingleFileOrEmptyDirectory(const std::string &path)
169 {
170         DWORD attr = GetFileAttributes(path.c_str());
171         bool is_directory = (attr != INVALID_FILE_ATTRIBUTES &&
172                         (attr & FILE_ATTRIBUTE_DIRECTORY));
173         if(!is_directory)
174         {
175                 bool did = DeleteFile(path.c_str());
176                 return did;
177         }
178         else
179         {
180                 bool did = RemoveDirectory(path.c_str());
181                 return did;
182         }
183 }
184
185 std::string TempPath()
186 {
187         DWORD bufsize = GetTempPath(0, NULL);
188         if(bufsize == 0){
189                 errorstream<<"GetTempPath failed, error = "<<GetLastError()<<std::endl;
190                 return "";
191         }
192         std::vector<char> buf(bufsize);
193         DWORD len = GetTempPath(bufsize, &buf[0]);
194         if(len == 0 || len > bufsize){
195                 errorstream<<"GetTempPath failed, error = "<<GetLastError()<<std::endl;
196                 return "";
197         }
198         return std::string(buf.begin(), buf.begin() + len);
199 }
200
201 #else // POSIX
202
203 #include <sys/types.h>
204 #include <dirent.h>
205 #include <sys/stat.h>
206 #include <sys/wait.h>
207 #include <unistd.h>
208
209 std::vector<DirListNode> GetDirListing(const std::string &pathstring)
210 {
211         std::vector<DirListNode> listing;
212
213         DIR *dp;
214         struct dirent *dirp;
215         if((dp = opendir(pathstring.c_str())) == NULL) {
216                 //infostream<<"Error("<<errno<<") opening "<<pathstring<<std::endl;
217                 return listing;
218         }
219
220         while ((dirp = readdir(dp)) != NULL) {
221                 // NOTE:
222                 // Be very sure to not include '..' in the results, it will
223                 // result in an epic failure when deleting stuff.
224                 if(strcmp(dirp->d_name, ".") == 0 || strcmp(dirp->d_name, "..") == 0)
225                         continue;
226
227                 DirListNode node;
228                 node.name = dirp->d_name;
229
230                 int isdir = -1; // -1 means unknown
231
232                 /*
233                         POSIX doesn't define d_type member of struct dirent and
234                         certain filesystems on glibc/Linux will only return
235                         DT_UNKNOWN for the d_type member.
236
237                         Also we don't know whether symlinks are directories or not.
238                 */
239 #ifdef _DIRENT_HAVE_D_TYPE
240                 if(dirp->d_type != DT_UNKNOWN && dirp->d_type != DT_LNK)
241                         isdir = (dirp->d_type == DT_DIR);
242 #endif /* _DIRENT_HAVE_D_TYPE */
243
244                 /*
245                         Was d_type DT_UNKNOWN, DT_LNK or nonexistent?
246                         If so, try stat().
247                 */
248                 if(isdir == -1) {
249                         struct stat statbuf{};
250                         if (stat((pathstring + "/" + node.name).c_str(), &statbuf))
251                                 continue;
252                         isdir = ((statbuf.st_mode & S_IFDIR) == S_IFDIR);
253                 }
254                 node.dir = isdir;
255                 listing.push_back(node);
256         }
257         closedir(dp);
258
259         return listing;
260 }
261
262 bool CreateDir(const std::string &path)
263 {
264         int r = mkdir(path.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
265         if (r == 0) {
266                 return true;
267         }
268
269         // If already exists, return true
270         if (errno == EEXIST)
271                 return true;
272         return false;
273
274 }
275
276 bool PathExists(const std::string &path)
277 {
278         struct stat st{};
279         return (stat(path.c_str(),&st) == 0);
280 }
281
282 bool IsPathAbsolute(const std::string &path)
283 {
284         return path[0] == '/';
285 }
286
287 bool IsDir(const std::string &path)
288 {
289         struct stat statbuf{};
290         if(stat(path.c_str(), &statbuf))
291                 return false; // Actually error; but certainly not a directory
292         return ((statbuf.st_mode & S_IFDIR) == S_IFDIR);
293 }
294
295 bool IsDirDelimiter(char c)
296 {
297         return c == '/';
298 }
299
300 bool RecursiveDelete(const std::string &path)
301 {
302         /*
303                 Execute the 'rm' command directly, by fork() and execve()
304         */
305
306         infostream<<"Removing \""<<path<<"\""<<std::endl;
307
308         //return false;
309
310         pid_t child_pid = fork();
311
312         if(child_pid == 0)
313         {
314                 // Child
315                 char argv_data[3][10000];
316                 strcpy(argv_data[0], "/bin/rm");
317                 strcpy(argv_data[1], "-rf");
318                 strncpy(argv_data[2], path.c_str(), 10000);
319                 char *argv[4];
320                 argv[0] = argv_data[0];
321                 argv[1] = argv_data[1];
322                 argv[2] = argv_data[2];
323                 argv[3] = NULL;
324
325                 verbosestream<<"Executing '"<<argv[0]<<"' '"<<argv[1]<<"' '"
326                                 <<argv[2]<<"'"<<std::endl;
327
328                 execv(argv[0], argv);
329
330                 // Execv shouldn't return. Failed.
331                 _exit(1);
332         }
333         else
334         {
335                 // Parent
336                 int child_status;
337                 pid_t tpid;
338                 do{
339                         tpid = wait(&child_status);
340                         //if(tpid != child_pid) process_terminated(tpid);
341                 }while(tpid != child_pid);
342                 return (child_status == 0);
343         }
344 }
345
346 bool DeleteSingleFileOrEmptyDirectory(const std::string &path)
347 {
348         if (IsDir(path)) {
349                 bool did = (rmdir(path.c_str()) == 0);
350                 if (!did)
351                         errorstream << "rmdir errno: " << errno << ": " << strerror(errno)
352                                         << std::endl;
353                 return did;
354         }
355
356         bool did = (unlink(path.c_str()) == 0);
357         if (!did)
358                 errorstream << "unlink errno: " << errno << ": " << strerror(errno)
359                                 << std::endl;
360         return did;
361 }
362
363 std::string TempPath()
364 {
365         /*
366                 Should the environment variables TMPDIR, TMP and TEMP
367                 and the macro P_tmpdir (if defined by stdio.h) be checked
368                 before falling back on /tmp?
369
370                 Probably not, because this function is intended to be
371                 compatible with lua's os.tmpname which under the default
372                 configuration hardcodes mkstemp("/tmp/lua_XXXXXX").
373         */
374 #ifdef __ANDROID__
375         return DIR_DELIM "sdcard" DIR_DELIM PROJECT_NAME DIR_DELIM "tmp";
376 #else
377         return DIR_DELIM "tmp";
378 #endif
379 }
380
381 #endif
382
383 void GetRecursiveDirs(std::vector<std::string> &dirs, const std::string &dir)
384 {
385         static const std::set<char> chars_to_ignore = { '_', '.' };
386         if (dir.empty() || !IsDir(dir))
387                 return;
388         dirs.push_back(dir);
389         fs::GetRecursiveSubPaths(dir, dirs, false, chars_to_ignore);
390 }
391
392 std::vector<std::string> GetRecursiveDirs(const std::string &dir)
393 {
394         std::vector<std::string> result;
395         GetRecursiveDirs(result, dir);
396         return result;
397 }
398
399 void GetRecursiveSubPaths(const std::string &path,
400                   std::vector<std::string> &dst,
401                   bool list_files,
402                   const std::set<char> &ignore)
403 {
404         std::vector<DirListNode> content = GetDirListing(path);
405         for (const auto &n : content) {
406                 std::string fullpath = path + DIR_DELIM + n.name;
407                 if (ignore.count(n.name[0]))
408                         continue;
409                 if (list_files || n.dir)
410                         dst.push_back(fullpath);
411                 if (n.dir)
412                         GetRecursiveSubPaths(fullpath, dst, list_files, ignore);
413         }
414 }
415
416 bool DeletePaths(const std::vector<std::string> &paths)
417 {
418         bool success = true;
419         // Go backwards to succesfully delete the output of GetRecursiveSubPaths
420         for(int i=paths.size()-1; i>=0; i--){
421                 const std::string &path = paths[i];
422                 bool did = DeleteSingleFileOrEmptyDirectory(path);
423                 if(!did){
424                         errorstream<<"Failed to delete "<<path<<std::endl;
425                         success = false;
426                 }
427         }
428         return success;
429 }
430
431 bool RecursiveDeleteContent(const std::string &path)
432 {
433         infostream<<"Removing content of \""<<path<<"\""<<std::endl;
434         std::vector<DirListNode> list = GetDirListing(path);
435         for (const DirListNode &dln : list) {
436                 if(trim(dln.name) == "." || trim(dln.name) == "..")
437                         continue;
438                 std::string childpath = path + DIR_DELIM + dln.name;
439                 bool r = RecursiveDelete(childpath);
440                 if(!r) {
441                         errorstream << "Removing \"" << childpath << "\" failed" << std::endl;
442                         return false;
443                 }
444         }
445         return true;
446 }
447
448 bool CreateAllDirs(const std::string &path)
449 {
450
451         std::vector<std::string> tocreate;
452         std::string basepath = path;
453         while(!PathExists(basepath))
454         {
455                 tocreate.push_back(basepath);
456                 basepath = RemoveLastPathComponent(basepath);
457                 if(basepath.empty())
458                         break;
459         }
460         for(int i=tocreate.size()-1;i>=0;i--)
461                 if(!CreateDir(tocreate[i]))
462                         return false;
463         return true;
464 }
465
466 bool CopyFileContents(const std::string &source, const std::string &target)
467 {
468         FILE *sourcefile = fopen(source.c_str(), "rb");
469         if(sourcefile == NULL){
470                 errorstream<<source<<": can't open for reading: "
471                         <<strerror(errno)<<std::endl;
472                 return false;
473         }
474
475         FILE *targetfile = fopen(target.c_str(), "wb");
476         if(targetfile == NULL){
477                 errorstream<<target<<": can't open for writing: "
478                         <<strerror(errno)<<std::endl;
479                 fclose(sourcefile);
480                 return false;
481         }
482
483         size_t total = 0;
484         bool retval = true;
485         bool done = false;
486         char readbuffer[BUFSIZ];
487         while(!done){
488                 size_t readbytes = fread(readbuffer, 1,
489                                 sizeof(readbuffer), sourcefile);
490                 total += readbytes;
491                 if(ferror(sourcefile)){
492                         errorstream<<source<<": IO error: "
493                                 <<strerror(errno)<<std::endl;
494                         retval = false;
495                         done = true;
496                 }
497                 if(readbytes > 0){
498                         fwrite(readbuffer, 1, readbytes, targetfile);
499                 }
500                 if(feof(sourcefile) || ferror(sourcefile)){
501                         // flush destination file to catch write errors
502                         // (e.g. disk full)
503                         fflush(targetfile);
504                         done = true;
505                 }
506                 if(ferror(targetfile)){
507                         errorstream<<target<<": IO error: "
508                                         <<strerror(errno)<<std::endl;
509                         retval = false;
510                         done = true;
511                 }
512         }
513         infostream<<"copied "<<total<<" bytes from "
514                 <<source<<" to "<<target<<std::endl;
515         fclose(sourcefile);
516         fclose(targetfile);
517         return retval;
518 }
519
520 bool CopyDir(const std::string &source, const std::string &target)
521 {
522         if(PathExists(source)){
523                 if(!PathExists(target)){
524                         fs::CreateAllDirs(target);
525                 }
526                 bool retval = true;
527                 std::vector<DirListNode> content = fs::GetDirListing(source);
528
529                 for (const auto &dln : content) {
530                         std::string sourcechild = source + DIR_DELIM + dln.name;
531                         std::string targetchild = target + DIR_DELIM + dln.name;
532                         if(dln.dir){
533                                 if(!fs::CopyDir(sourcechild, targetchild)){
534                                         retval = false;
535                                 }
536                         }
537                         else {
538                                 if(!fs::CopyFileContents(sourcechild, targetchild)){
539                                         retval = false;
540                                 }
541                         }
542                 }
543                 return retval;
544         }
545
546         return false;
547 }
548
549 bool PathStartsWith(const std::string &path, const std::string &prefix)
550 {
551         size_t pathsize = path.size();
552         size_t pathpos = 0;
553         size_t prefixsize = prefix.size();
554         size_t prefixpos = 0;
555         for(;;){
556                 bool delim1 = pathpos == pathsize
557                         || IsDirDelimiter(path[pathpos]);
558                 bool delim2 = prefixpos == prefixsize
559                         || IsDirDelimiter(prefix[prefixpos]);
560
561                 if(delim1 != delim2)
562                         return false;
563
564                 if(delim1){
565                         while(pathpos < pathsize &&
566                                         IsDirDelimiter(path[pathpos]))
567                                 ++pathpos;
568                         while(prefixpos < prefixsize &&
569                                         IsDirDelimiter(prefix[prefixpos]))
570                                 ++prefixpos;
571                         if(prefixpos == prefixsize)
572                                 return true;
573                         if(pathpos == pathsize)
574                                 return false;
575                 }
576                 else{
577                         size_t len = 0;
578                         do{
579                                 char pathchar = path[pathpos+len];
580                                 char prefixchar = prefix[prefixpos+len];
581                                 if(FILESYS_CASE_INSENSITIVE){
582                                         pathchar = tolower(pathchar);
583                                         prefixchar = tolower(prefixchar);
584                                 }
585                                 if(pathchar != prefixchar)
586                                         return false;
587                                 ++len;
588                         } while(pathpos+len < pathsize
589                                         && !IsDirDelimiter(path[pathpos+len])
590                                         && prefixpos+len < prefixsize
591                                         && !IsDirDelimiter(
592                                                 prefix[prefixpos+len]));
593                         pathpos += len;
594                         prefixpos += len;
595                 }
596         }
597 }
598
599 std::string RemoveLastPathComponent(const std::string &path,
600                 std::string *removed, int count)
601 {
602         if(removed)
603                 *removed = "";
604
605         size_t remaining = path.size();
606
607         for(int i = 0; i < count; ++i){
608                 // strip a dir delimiter
609                 while(remaining != 0 && IsDirDelimiter(path[remaining-1]))
610                         remaining--;
611                 // strip a path component
612                 size_t component_end = remaining;
613                 while(remaining != 0 && !IsDirDelimiter(path[remaining-1]))
614                         remaining--;
615                 size_t component_start = remaining;
616                 // strip a dir delimiter
617                 while(remaining != 0 && IsDirDelimiter(path[remaining-1]))
618                         remaining--;
619                 if(removed){
620                         std::string component = path.substr(component_start,
621                                         component_end - component_start);
622                         if(i)
623                                 *removed = component + DIR_DELIM + *removed;
624                         else
625                                 *removed = component;
626                 }
627         }
628         return path.substr(0, remaining);
629 }
630
631 std::string RemoveRelativePathComponents(std::string path)
632 {
633         size_t pos = path.size();
634         size_t dotdot_count = 0;
635         while (pos != 0) {
636                 size_t component_with_delim_end = pos;
637                 // skip a dir delimiter
638                 while (pos != 0 && IsDirDelimiter(path[pos-1]))
639                         pos--;
640                 // strip a path component
641                 size_t component_end = pos;
642                 while (pos != 0 && !IsDirDelimiter(path[pos-1]))
643                         pos--;
644                 size_t component_start = pos;
645
646                 std::string component = path.substr(component_start,
647                                 component_end - component_start);
648                 bool remove_this_component = false;
649                 if (component == ".") {
650                         remove_this_component = true;
651                 } else if (component == "..") {
652                         remove_this_component = true;
653                         dotdot_count += 1;
654                 } else if (dotdot_count != 0) {
655                         remove_this_component = true;
656                         dotdot_count -= 1;
657                 }
658
659                 if (remove_this_component) {
660                         while (pos != 0 && IsDirDelimiter(path[pos-1]))
661                                 pos--;
662                         if (component_start == 0) {
663                                 // We need to remove the delemiter too
664                                 path = path.substr(component_with_delim_end, std::string::npos);
665                         } else {
666                                 path = path.substr(0, pos) + DIR_DELIM +
667                                         path.substr(component_with_delim_end, std::string::npos);
668                         }
669                         if (pos > 0)
670                                 pos++;
671                 }
672         }
673
674         if (dotdot_count > 0)
675                 return "";
676
677         // remove trailing dir delimiters
678         pos = path.size();
679         while (pos != 0 && IsDirDelimiter(path[pos-1]))
680                 pos--;
681         return path.substr(0, pos);
682 }
683
684 std::string AbsolutePath(const std::string &path)
685 {
686 #ifdef _WIN32
687         char *abs_path = _fullpath(NULL, path.c_str(), MAX_PATH);
688 #else
689         char *abs_path = realpath(path.c_str(), NULL);
690 #endif
691         if (!abs_path) return "";
692         std::string abs_path_str(abs_path);
693         free(abs_path);
694         return abs_path_str;
695 }
696
697 const char *GetFilenameFromPath(const char *path)
698 {
699         const char *filename = strrchr(path, DIR_DELIM_CHAR);
700         return filename ? filename + 1 : path;
701 }
702
703 bool safeWriteToFile(const std::string &path, const std::string &content)
704 {
705         std::string tmp_file = path + ".~mt";
706
707         // Write to a tmp file
708         std::ofstream os(tmp_file.c_str(), std::ios::binary);
709         if (!os.good())
710                 return false;
711         os << content;
712         os.flush();
713         os.close();
714         if (os.fail()) {
715                 // Remove the temporary file because writing it failed and it's useless.
716                 remove(tmp_file.c_str());
717                 return false;
718         }
719
720         bool rename_success = false;
721
722         // Move the finished temporary file over the real file
723 #ifdef _WIN32
724         // When creating the file, it can cause Windows Search indexer, virus scanners and other apps
725         // to query the file. This can make the move file call below fail.
726         // We retry up to 5 times, with a 1ms sleep between, before we consider the whole operation failed
727         int number_attempts = 0;
728         while (number_attempts < 5) {
729                 rename_success = MoveFileEx(tmp_file.c_str(), path.c_str(),
730                                 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
731                 if (rename_success)
732                         break;
733                 sleep_ms(1);
734                 ++number_attempts;
735         }
736 #else
737         // On POSIX compliant systems rename() is specified to be able to swap the
738         // file in place of the destination file, making this a truly error-proof
739         // transaction.
740         rename_success = rename(tmp_file.c_str(), path.c_str()) == 0;
741 #endif
742         if (!rename_success) {
743                 warningstream << "Failed to write to file: " << path.c_str() << std::endl;
744                 // Remove the temporary file because moving it over the target file
745                 // failed.
746                 remove(tmp_file.c_str());
747                 return false;
748         }
749
750         return true;
751 }
752
753 bool Rename(const std::string &from, const std::string &to)
754 {
755         return rename(from.c_str(), to.c_str()) == 0;
756 }
757
758 } // namespace fs
759