Fix fs::RemoveRelativePathComponents for paths with a leading dot component
[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 <stdio.h>
24 #include <string.h>
25 #include <errno.h>
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         {
267                 return true;
268         }
269         else
270         {
271                 // If already exists, return true
272                 if(errno == EEXIST)
273                         return true;
274                 return false;
275         }
276 }
277
278 bool PathExists(const std::string &path)
279 {
280         struct stat st;
281         return (stat(path.c_str(),&st) == 0);
282 }
283
284 bool IsPathAbsolute(const std::string &path)
285 {
286         return path[0] == '/';
287 }
288
289 bool IsDir(const std::string &path)
290 {
291         struct stat statbuf;
292         if(stat(path.c_str(), &statbuf))
293                 return false; // Actually error; but certainly not a directory
294         return ((statbuf.st_mode & S_IFDIR) == S_IFDIR);
295 }
296
297 bool IsDirDelimiter(char c)
298 {
299         return c == '/';
300 }
301
302 bool RecursiveDelete(const std::string &path)
303 {
304         /*
305                 Execute the 'rm' command directly, by fork() and execve()
306         */
307
308         infostream<<"Removing \""<<path<<"\""<<std::endl;
309
310         //return false;
311
312         pid_t child_pid = fork();
313
314         if(child_pid == 0)
315         {
316                 // Child
317                 char argv_data[3][10000];
318                 strcpy(argv_data[0], "/bin/rm");
319                 strcpy(argv_data[1], "-rf");
320                 strncpy(argv_data[2], path.c_str(), 10000);
321                 char *argv[4];
322                 argv[0] = argv_data[0];
323                 argv[1] = argv_data[1];
324                 argv[2] = argv_data[2];
325                 argv[3] = NULL;
326
327                 verbosestream<<"Executing '"<<argv[0]<<"' '"<<argv[1]<<"' '"
328                                 <<argv[2]<<"'"<<std::endl;
329
330                 execv(argv[0], argv);
331
332                 // Execv shouldn't return. Failed.
333                 _exit(1);
334         }
335         else
336         {
337                 // Parent
338                 int child_status;
339                 pid_t tpid;
340                 do{
341                         tpid = wait(&child_status);
342                         //if(tpid != child_pid) process_terminated(tpid);
343                 }while(tpid != child_pid);
344                 return (child_status == 0);
345         }
346 }
347
348 bool DeleteSingleFileOrEmptyDirectory(const std::string &path)
349 {
350         if(IsDir(path)){
351                 bool did = (rmdir(path.c_str()) == 0);
352                 if(!did)
353                         errorstream<<"rmdir errno: "<<errno<<": "<<strerror(errno)
354                                         <<std::endl;
355                 return did;
356         } else {
357                 bool did = (unlink(path.c_str()) == 0);
358                 if(!did)
359                         errorstream<<"unlink errno: "<<errno<<": "<<strerror(errno)
360                                         <<std::endl;
361                 return did;
362         }
363 }
364
365 std::string TempPath()
366 {
367         /*
368                 Should the environment variables TMPDIR, TMP and TEMP
369                 and the macro P_tmpdir (if defined by stdio.h) be checked
370                 before falling back on /tmp?
371
372                 Probably not, because this function is intended to be
373                 compatible with lua's os.tmpname which under the default
374                 configuration hardcodes mkstemp("/tmp/lua_XXXXXX").
375         */
376 #ifdef __ANDROID__
377         return DIR_DELIM "sdcard" DIR_DELIM PROJECT_NAME DIR_DELIM "tmp";
378 #else
379         return DIR_DELIM "tmp";
380 #endif
381 }
382
383 #endif
384
385 void GetRecursiveSubPaths(const std::string &path, std::vector<std::string> &dst)
386 {
387         std::vector<DirListNode> content = GetDirListing(path);
388         for(unsigned int  i=0; i<content.size(); i++){
389                 const DirListNode &n = content[i];
390                 std::string fullpath = path + DIR_DELIM + n.name;
391                 dst.push_back(fullpath);
392                 if (n.dir) {
393                         GetRecursiveSubPaths(fullpath, dst);
394                 }
395         }
396 }
397
398 bool DeletePaths(const std::vector<std::string> &paths)
399 {
400         bool success = true;
401         // Go backwards to succesfully delete the output of GetRecursiveSubPaths
402         for(int i=paths.size()-1; i>=0; i--){
403                 const std::string &path = paths[i];
404                 bool did = DeleteSingleFileOrEmptyDirectory(path);
405                 if(!did){
406                         errorstream<<"Failed to delete "<<path<<std::endl;
407                         success = false;
408                 }
409         }
410         return success;
411 }
412
413 bool RecursiveDeleteContent(const std::string &path)
414 {
415         infostream<<"Removing content of \""<<path<<"\""<<std::endl;
416         std::vector<DirListNode> list = GetDirListing(path);
417         for(unsigned int i=0; i<list.size(); i++)
418         {
419                 if(trim(list[i].name) == "." || trim(list[i].name) == "..")
420                         continue;
421                 std::string childpath = path + DIR_DELIM + list[i].name;
422                 bool r = RecursiveDelete(childpath);
423                 if(r == false)
424                 {
425                         errorstream<<"Removing \""<<childpath<<"\" failed"<<std::endl;
426                         return false;
427                 }
428         }
429         return true;
430 }
431
432 bool CreateAllDirs(const std::string &path)
433 {
434
435         std::vector<std::string> tocreate;
436         std::string basepath = path;
437         while(!PathExists(basepath))
438         {
439                 tocreate.push_back(basepath);
440                 basepath = RemoveLastPathComponent(basepath);
441                 if(basepath.empty())
442                         break;
443         }
444         for(int i=tocreate.size()-1;i>=0;i--)
445                 if(!CreateDir(tocreate[i]))
446                         return false;
447         return true;
448 }
449
450 bool CopyFileContents(const std::string &source, const std::string &target)
451 {
452         FILE *sourcefile = fopen(source.c_str(), "rb");
453         if(sourcefile == NULL){
454                 errorstream<<source<<": can't open for reading: "
455                         <<strerror(errno)<<std::endl;
456                 return false;
457         }
458
459         FILE *targetfile = fopen(target.c_str(), "wb");
460         if(targetfile == NULL){
461                 errorstream<<target<<": can't open for writing: "
462                         <<strerror(errno)<<std::endl;
463                 fclose(sourcefile);
464                 return false;
465         }
466
467         size_t total = 0;
468         bool retval = true;
469         bool done = false;
470         char readbuffer[BUFSIZ];
471         while(!done){
472                 size_t readbytes = fread(readbuffer, 1,
473                                 sizeof(readbuffer), sourcefile);
474                 total += readbytes;
475                 if(ferror(sourcefile)){
476                         errorstream<<source<<": IO error: "
477                                 <<strerror(errno)<<std::endl;
478                         retval = false;
479                         done = true;
480                 }
481                 if(readbytes > 0){
482                         fwrite(readbuffer, 1, readbytes, targetfile);
483                 }
484                 if(feof(sourcefile) || ferror(sourcefile)){
485                         // flush destination file to catch write errors
486                         // (e.g. disk full)
487                         fflush(targetfile);
488                         done = true;
489                 }
490                 if(ferror(targetfile)){
491                         errorstream<<target<<": IO error: "
492                                         <<strerror(errno)<<std::endl;
493                         retval = false;
494                         done = true;
495                 }
496         }
497         infostream<<"copied "<<total<<" bytes from "
498                 <<source<<" to "<<target<<std::endl;
499         fclose(sourcefile);
500         fclose(targetfile);
501         return retval;
502 }
503
504 bool CopyDir(const std::string &source, const std::string &target)
505 {
506         if(PathExists(source)){
507                 if(!PathExists(target)){
508                         fs::CreateAllDirs(target);
509                 }
510                 bool retval = true;
511                 std::vector<DirListNode> content = fs::GetDirListing(source);
512
513                 for(unsigned int i=0; i < content.size(); i++){
514                         std::string sourcechild = source + DIR_DELIM + content[i].name;
515                         std::string targetchild = target + DIR_DELIM + content[i].name;
516                         if(content[i].dir){
517                                 if(!fs::CopyDir(sourcechild, targetchild)){
518                                         retval = false;
519                                 }
520                         }
521                         else {
522                                 if(!fs::CopyFileContents(sourcechild, targetchild)){
523                                         retval = false;
524                                 }
525                         }
526                 }
527                 return retval;
528         }
529         else {
530                 return false;
531         }
532 }
533
534 bool PathStartsWith(const std::string &path, const std::string &prefix)
535 {
536         size_t pathsize = path.size();
537         size_t pathpos = 0;
538         size_t prefixsize = prefix.size();
539         size_t prefixpos = 0;
540         for(;;){
541                 bool delim1 = pathpos == pathsize
542                         || IsDirDelimiter(path[pathpos]);
543                 bool delim2 = prefixpos == prefixsize
544                         || IsDirDelimiter(prefix[prefixpos]);
545
546                 if(delim1 != delim2)
547                         return false;
548
549                 if(delim1){
550                         while(pathpos < pathsize &&
551                                         IsDirDelimiter(path[pathpos]))
552                                 ++pathpos;
553                         while(prefixpos < prefixsize &&
554                                         IsDirDelimiter(prefix[prefixpos]))
555                                 ++prefixpos;
556                         if(prefixpos == prefixsize)
557                                 return true;
558                         if(pathpos == pathsize)
559                                 return false;
560                 }
561                 else{
562                         size_t len = 0;
563                         do{
564                                 char pathchar = path[pathpos+len];
565                                 char prefixchar = prefix[prefixpos+len];
566                                 if(FILESYS_CASE_INSENSITIVE){
567                                         pathchar = tolower(pathchar);
568                                         prefixchar = tolower(prefixchar);
569                                 }
570                                 if(pathchar != prefixchar)
571                                         return false;
572                                 ++len;
573                         } while(pathpos+len < pathsize
574                                         && !IsDirDelimiter(path[pathpos+len])
575                                         && prefixpos+len < prefixsize
576                                         && !IsDirDelimiter(
577                                                 prefix[prefixpos+len]));
578                         pathpos += len;
579                         prefixpos += len;
580                 }
581         }
582 }
583
584 std::string RemoveLastPathComponent(const std::string &path,
585                 std::string *removed, int count)
586 {
587         if(removed)
588                 *removed = "";
589
590         size_t remaining = path.size();
591
592         for(int i = 0; i < count; ++i){
593                 // strip a dir delimiter
594                 while(remaining != 0 && IsDirDelimiter(path[remaining-1]))
595                         remaining--;
596                 // strip a path component
597                 size_t component_end = remaining;
598                 while(remaining != 0 && !IsDirDelimiter(path[remaining-1]))
599                         remaining--;
600                 size_t component_start = remaining;
601                 // strip a dir delimiter
602                 while(remaining != 0 && IsDirDelimiter(path[remaining-1]))
603                         remaining--;
604                 if(removed){
605                         std::string component = path.substr(component_start,
606                                         component_end - component_start);
607                         if(i)
608                                 *removed = component + DIR_DELIM + *removed;
609                         else
610                                 *removed = component;
611                 }
612         }
613         return path.substr(0, remaining);
614 }
615
616 std::string RemoveRelativePathComponents(std::string path)
617 {
618         size_t pos = path.size();
619         size_t dotdot_count = 0;
620         while(pos != 0){
621                 size_t component_with_delim_end = pos;
622                 // skip a dir delimiter
623                 while(pos != 0 && IsDirDelimiter(path[pos-1]))
624                         pos--;
625                 // strip a path component
626                 size_t component_end = pos;
627                 while(pos != 0 && !IsDirDelimiter(path[pos-1]))
628                         pos--;
629                 size_t component_start = pos;
630
631                 std::string component = path.substr(component_start,
632                                 component_end - component_start);
633                 bool remove_this_component = false;
634                 if(component == "." && component_start != 0){
635                         remove_this_component = true;
636                 }
637                 else if(component == ".."){
638                         remove_this_component = true;
639                         dotdot_count += 1;
640                 }
641                 else if(dotdot_count != 0){
642                         remove_this_component = true;
643                         dotdot_count -= 1;
644                 }
645
646                 if(remove_this_component){
647                         while(pos != 0 && IsDirDelimiter(path[pos-1]))
648                                 pos--;
649                         path = path.substr(0, pos) + DIR_DELIM +
650                                 path.substr(component_with_delim_end,
651                                                 std::string::npos);
652                         pos++;
653                 }
654         }
655
656         if(dotdot_count > 0)
657                 return "";
658
659         // remove trailing dir delimiters
660         pos = path.size();
661         while(pos != 0 && IsDirDelimiter(path[pos-1]))
662                 pos--;
663         return path.substr(0, pos);
664 }
665
666 std::string AbsolutePath(const std::string &path)
667 {
668 #ifdef _WIN32
669         char *abs_path = _fullpath(NULL, path.c_str(), MAX_PATH);
670 #else
671         char *abs_path = realpath(path.c_str(), NULL);
672 #endif
673         if (!abs_path) return "";
674         std::string abs_path_str(abs_path);
675         free(abs_path);
676         return abs_path_str;
677 }
678
679 const char *GetFilenameFromPath(const char *path)
680 {
681         const char *filename = strrchr(path, DIR_DELIM_CHAR);
682         return filename ? filename + 1 : path;
683 }
684
685 bool safeWriteToFile(const std::string &path, const std::string &content)
686 {
687         std::string tmp_file = path + ".~mt";
688
689         // Write to a tmp file
690         std::ofstream os(tmp_file.c_str(), std::ios::binary);
691         if (!os.good())
692                 return false;
693         os << content;
694         os.flush();
695         os.close();
696         if (os.fail()) {
697                 // Remove the temporary file because writing it failed and it's useless.
698                 remove(tmp_file.c_str());
699                 return false;
700         }
701
702         bool rename_success = false;
703
704         // Move the finished temporary file over the real file
705 #ifdef _WIN32
706         // When creating the file, it can cause Windows Search indexer, virus scanners and other apps
707         // to query the file. This can make the move file call below fail.
708         // We retry up to 5 times, with a 1ms sleep between, before we consider the whole operation failed
709         int number_attempts = 0;
710         while (number_attempts < 5) {
711                 rename_success = MoveFileEx(tmp_file.c_str(), path.c_str(),
712                                 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
713                 if (rename_success)
714                         break;
715                 sleep_ms(1);
716                 ++number_attempts;
717         }
718 #else
719         // On POSIX compliant systems rename() is specified to be able to swap the
720         // file in place of the destination file, making this a truly error-proof
721         // transaction.
722         rename_success = rename(tmp_file.c_str(), path.c_str()) == 0;
723 #endif
724         if (!rename_success) {
725                 warningstream << "Failed to write to file: " << path.c_str() << std::endl;
726                 // Remove the temporary file because moving it over the target file
727                 // failed.
728                 remove(tmp_file.c_str());
729                 return false;
730         }
731
732         return true;
733 }
734
735 bool Rename(const std::string &from, const std::string &to)
736 {
737         return rename(from.c_str(), to.c_str()) == 0;
738 }
739
740 } // namespace fs
741