1 #include "io/FileSystem.h"
2 #include "objects/containers/StaticArray.h"
3 #include "objects/utility/Streams.h"
4 #include <sstream>
5 
6 #ifdef SPH_WIN
7 #include <userenv.h>
8 #include <windows.h>
9 #else
10 #include <dirent.h>
11 #include <sys/file.h>
12 #include <sys/stat.h>
13 #include <sys/types.h>
14 #include <unistd.h>
15 #endif
16 #include <climits>	// for PATH_MAX
17 
18 NAMESPACE_SPH_BEGIN
19 
20 #ifdef SPH_WIN
21 
22 /// Returns the error message corresponding to GetLastError().
getLastErrorMessage()23 static String getLastErrorMessage() {
24     DWORD error = GetLastError();
25     if (!error) {
26         return {};
27     }
28 
29     wchar_t message[256] = { 0 };
30     DWORD flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;
31     DWORD length =
32         FormatMessageW(flags, NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), message, 256, nullptr);
33     if (length) {
34         return String(message);
35     } else {
36         return {};
37     }
38 }
39 
40 #endif
41 
readFile(const Path & path)42 String FileSystem::readFile(const Path& path) {
43     FileTextInputStream stream(path);
44     String text;
45     if (stream.readAll(text)) {
46         return text;
47     } else {
48         return {};
49     }
50 }
51 
pathExists(const Path & path)52 bool FileSystem::pathExists(const Path& path) {
53 #ifndef SPH_WIN
54     struct stat buffer;
55     if (path.empty()) {
56         return false;
57     }
58     return (stat(path.native(), &buffer) == 0);
59 #else
60     DWORD attributes = GetFileAttributesW(path.native());
61     return attributes != INVALID_FILE_ATTRIBUTES;
62 #endif
63 }
64 
fileSize(const Path & path)65 std::size_t FileSystem::fileSize(const Path& path) {
66     std::ifstream ifs(path.native(), std::ios::ate | std::ios::binary);
67     SPH_ASSERT(ifs);
68     return ifs.tellg();
69 }
70 
isDirectoryWritable(const Path & path)71 bool FileSystem::isDirectoryWritable(const Path& path) {
72     SPH_ASSERT(pathType(path).valueOr(PathType::OTHER) == PathType::DIRECTORY);
73 #ifndef SPH_WIN
74     return access(path.native(), W_OK) == 0;
75 #else
76     wchar_t file[MAX_PATH];
77     GetTempFileNameW(path.native(), L"sph", 1, file);
78     HANDLE handle = CreateFileW(file,
79         GENERIC_WRITE,
80         FILE_SHARE_READ,
81         nullptr,
82         CREATE_NEW,
83         FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE,
84         nullptr);
85     if (handle != INVALID_HANDLE_VALUE) {
86         CloseHandle(handle);
87         return true;
88     } else {
89         return false;
90     }
91 #endif
92 }
93 
getHomeDirectory()94 Expected<Path> FileSystem::getHomeDirectory() {
95 #ifdef SPH_WIN
96     wchar_t buffer[MAX_PATH] = { 0 };
97     DWORD length = sizeof(buffer);
98     HANDLE token = 0;
99     OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token);
100     if (GetUserProfileDirectoryW(token, buffer, &length)) {
101         CloseHandle(token);
102         return Path(String(buffer) + L'\\');
103     } else {
104         return makeUnexpected<Path>(getLastErrorMessage());
105     }
106 #else
107     const char* homeDir = getenv("HOME");
108     if (homeDir != nullptr) {
109         String path = String::fromUtf8(homeDir) + L'/';
110         return Path(path);
111     } else {
112         return makeUnexpected<Path>("Cannot obtain home directory");
113     }
114 #endif
115 }
116 
getUserDataDirectory()117 Expected<Path> FileSystem::getUserDataDirectory() {
118     Expected<Path> homeDir = getHomeDirectory();
119     if (homeDir) {
120 #ifdef SPH_WIN
121         return homeDir.value();
122 #else
123         return homeDir.value() / Path(".config");
124 #endif
125     } else {
126         return homeDir;
127     }
128 }
129 
getAbsolutePath(const Path & relativePath)130 Expected<Path> FileSystem::getAbsolutePath(const Path& relativePath) {
131 #ifndef SPH_WIN
132     char realPath[PATH_MAX];
133     if (realpath(relativePath.native(), realPath)) {
134         return Path(String::fromUtf8(realPath));
135     } else {
136         switch (errno) {
137         case EACCES:
138             return makeUnexpected<Path>(
139                 "Read or search permission was denied for a component of the path prefix.");
140         case EINVAL:
141             return makeUnexpected<Path>("Path is NULL.");
142         case EIO:
143             return makeUnexpected<Path>("An I/O error occurred while reading from the filesystem.");
144         case ELOOP:
145             return makeUnexpected<Path>(
146                 "Too many symbolic links were encountered in translating the pathname.");
147         case ENAMETOOLONG:
148             return makeUnexpected<Path>(
149                 "A component of a pathname exceeded NAME_MAX characters, or an entire pathname exceeded "
150                 "PATH_MAX characters.");
151         case ENOENT:
152             return makeUnexpected<Path>("The named file does not exist");
153         case ENOMEM:
154             return makeUnexpected<Path>("Out of memory");
155         case ENOTDIR:
156             return makeUnexpected<Path>("A component of the path prefix is not a directory.");
157         default:
158             return makeUnexpected<Path>("Unknown error");
159         }
160     }
161 #else
162     wchar_t buffer[256] = { 0 };
163 
164     DWORD retval = GetFullPathNameW(relativePath.native(), 256, buffer, nullptr);
165     if (retval) {
166         return Path(buffer);
167     } else {
168         return makeUnexpected<Path>(getLastErrorMessage());
169     }
170 #endif
171 }
172 
pathType(const Path & path)173 Expected<FileSystem::PathType> FileSystem::pathType(const Path& path) {
174     if (path.empty()) {
175         return makeUnexpected<PathType>("Path is empty");
176     }
177 
178 #ifndef SPH_WIN
179     struct stat buffer;
180     if (stat(path.native(), &buffer) != 0) {
181         /// \todo possibly get the actual error, like in other functions
182         return makeUnexpected<PathType>("Cannot retrieve type of the path");
183     }
184     if (S_ISREG(buffer.st_mode)) {
185         return PathType::FILE;
186     }
187     if (S_ISDIR(buffer.st_mode)) {
188         return PathType::DIRECTORY;
189     }
190     if (S_ISLNK(buffer.st_mode)) {
191         return PathType::SYMLINK;
192     }
193     return PathType::OTHER;
194 #else
195     DWORD attributes = GetFileAttributesW(path.native());
196     if (attributes == INVALID_FILE_ATTRIBUTES) {
197         return makeUnexpected<PathType>(getLastErrorMessage());
198     }
199     if (attributes & FILE_ATTRIBUTE_DIRECTORY) {
200         return PathType::DIRECTORY;
201     }
202     if (attributes & FILE_ATTRIBUTE_REPARSE_POINT) {
203         return PathType::SYMLINK;
204     }
205     return PathType::FILE;
206 #endif
207 }
208 
createSingleDirectory(const Path & path,const Flags<FileSystem::CreateDirectoryFlag> flags)209 static Outcome createSingleDirectory(const Path& path, const Flags<FileSystem::CreateDirectoryFlag> flags) {
210     SPH_ASSERT(!path.empty());
211 #ifndef SPH_WIN
212     const bool result = mkdir(path.native(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == 0;
213     if (!result) {
214         switch (errno) {
215         case EACCES:
216             return makeFailed(
217                 "Search permission is denied on a component of the path prefix, or write permission is "
218                 "denied on the parent directory of the directory to be created.");
219         case EEXIST:
220             if (flags.has(FileSystem::CreateDirectoryFlag::ALLOW_EXISTING)) {
221                 return SUCCESS;
222             } else {
223                 return makeFailed("The named file exists.");
224             }
225         case ELOOP:
226             return makeFailed(
227                 "A loop exists in symbolic links encountered during resolution of the path argument.");
228         case EMLINK:
229             return makeFailed("The link count of the parent directory would exceed {LINK_MAX}.");
230         case ENAMETOOLONG:
231             return makeFailed(
232                 "The length of the path argument exceeds {PATH_MAX} or a pathname component is longer "
233                 "than {NAME_MAX}.");
234         case ENOENT:
235             return makeFailed(
236                 "A component of the path prefix specified by path does not name an existing directory "
237                 "or path is an empty string.");
238         case ENOSPC:
239             return makeFailed(
240                 "The file system does not contain enough space to hold the contents of the new "
241                 "directory or to extend the parent directory of the new directory.");
242         case ENOTDIR:
243             return makeFailed("A component of the path prefix is not a directory.");
244         case EROFS:
245             return makeFailed("The parent directory resides on a read-only file system.");
246         default:
247             return makeFailed("Unknown error");
248         }
249     }
250     return SUCCESS;
251 #else
252     if (CreateDirectoryW(path.native(), nullptr)) {
253         return SUCCESS;
254     } else {
255         if (GetLastError() == ERROR_ALREADY_EXISTS &&
256             flags.has(FileSystem::CreateDirectoryFlag::ALLOW_EXISTING)) {
257             return SUCCESS;
258         }
259         return makeFailed(getLastErrorMessage());
260     }
261 #endif
262 }
263 
createDirectory(const Path & path,const Flags<CreateDirectoryFlag> flags)264 Outcome FileSystem::createDirectory(const Path& path, const Flags<CreateDirectoryFlag> flags) {
265     if (path.empty()) {
266         return SUCCESS;
267     }
268     const Path parentPath = path.parentPath();
269     if (!parentPath.empty() && !pathExists(parentPath)) {
270         const Outcome result = createDirectory(parentPath, flags);
271         if (!result) {
272             return result;
273         }
274     }
275     return createSingleDirectory(path, flags);
276 }
277 
removePath(const Path & path,const Flags<RemovePathFlag> flags)278 Outcome FileSystem::removePath(const Path& path, const Flags<RemovePathFlag> flags) {
279     if (path.empty()) {
280         return makeFailed("Attempting to remove an empty path");
281     }
282     if (path.isRoot()) {
283         return makeFailed("Attemping to remove root. Pls, don't ...");
284     }
285     if (!pathExists(path)) {
286         return makeFailed("Attemping to remove nonexisting path");
287     }
288     const Expected<PathType> type = pathType(path);
289     if (!type) {
290         return makeFailed(type.error());
291     }
292     if (type.value() == PathType::DIRECTORY && flags.has(RemovePathFlag::RECURSIVE)) {
293         for (Path child : iterateDirectory(path)) {
294             Outcome result = removePath(path / child, flags);
295             if (!result) {
296                 return result;
297             }
298         }
299     }
300 
301 #ifndef SPH_WIN
302     const bool result = (remove(path.native()) == 0);
303     if (!result) {
304         switch (errno) {
305         case EACCES:
306             return makeFailed(
307                 "Write access to the directory containing pathname was not allowed, or one of the "
308                 "directories in the path prefix of pathname did not allow search permission.");
309         case EBUSY:
310             return makeFailed(
311                 "Path " + path.string() +
312                 "is currently in use by the system or some process that prevents its removal.On "
313                 "Linux this means pathname is currently used as a mount point or is the root directory of "
314                 "the calling process.");
315         case EFAULT:
316             return makeFailed("Path " + path.string() + " points outside your accessible address space.");
317         case EINVAL:
318             return makeFailed("Path " + path.string() + " has . as last component.");
319         case ELOOP:
320             return makeFailed("Too many symbolic links were encountered in resolving path " + path.string());
321         case ENAMETOOLONG:
322             return makeFailed("Path " + path.string() + " was too long.");
323         case ENOENT:
324             return makeFailed("A directory component in path " + path.string() +
325                               " does not exist or is a dangling symbolic link.");
326         case ENOMEM:
327             return makeFailed("Insufficient kernel memory was available.");
328         case ENOTDIR:
329             return makeFailed(
330                 "Path " + path.string() +
331                 " or a component used as a directory in pathname, is not, in fact, a directory.");
332         case ENOTEMPTY:
333             return makeFailed(
334                 "Path " + path.string() +
335                 " contains entries other than . and ..; or, pathname has .. as its final component.");
336         case EPERM:
337             return makeFailed(
338                 "The directory containing path " + path.string() +
339                 " has the sticky bit(S_ISVTX) set and the process's "
340                 "effective user ID is neither the user ID of the file to be deleted nor that of the "
341                 "directory containing it, and the process is not privileged (Linux: does not have the "
342                 "CAP_FOWNER capability).");
343         case EROFS:
344             return makeFailed("Path " + path.string() + " refers to a directory on a read-only file system.");
345         default:
346             return makeFailed("Unknown error for path " + path.string());
347         }
348     }
349     return SUCCESS;
350 #else
351     BOOL result;
352     if (type.value() == PathType::DIRECTORY) {
353         result = RemoveDirectoryW(path.native());
354     } else if (type.value() == PathType::FILE) {
355         result = DeleteFileW(path.native());
356     } else {
357         NOT_IMPLEMENTED; // removing symlinks?
358     }
359 
360     if (result) {
361         return SUCCESS;
362     } else {
363         return makeFailed(getLastErrorMessage());
364     }
365 #endif
366 }
367 
copyFile(const Path & from,const Path & to)368 Outcome FileSystem::copyFile(const Path& from, const Path& to) {
369     SPH_ASSERT(pathType(from).valueOr(PathType::OTHER) == PathType::FILE);
370     // there doesn't seem to be any system function for copying, so let's do it by hand
371     std::ifstream ifs(from.native(), std::ios::in | std::ios::binary);
372     if (!ifs) {
373         return makeFailed("Cannon open file " + from.string() + " for reading");
374     }
375     Outcome result = createDirectory(to.parentPath());
376     if (!result) {
377         return result;
378     }
379     std::ofstream ofs(to.native(), std::ios::out | std::ios::binary);
380     if (!ofs) {
381         return makeFailed("Cannot open file " + to.string() + " for writing");
382     }
383 
384     StaticArray<char, 1024> buffer;
385     do {
386         ifs.read(&buffer[0], buffer.size());
387         ofs.write(&buffer[0], ifs.gcount());
388         if (!ofs) {
389             return makeFailed("Failed from copy the file");
390         }
391     } while (ifs);
392     return SUCCESS;
393 }
394 
copyDirectory(const Path & from,const Path & to)395 Outcome FileSystem::copyDirectory(const Path& from, const Path& to) {
396     SPH_ASSERT(pathType(from).valueOr(PathType::OTHER) == PathType::DIRECTORY);
397     Outcome result = createDirectory(to);
398     if (!result) {
399         return result;
400     }
401     for (Path path : iterateDirectory(from)) {
402         const PathType type = pathType(from / path).valueOr(PathType::OTHER);
403         switch (type) {
404         case PathType::FILE:
405             result = copyFile(from / path, to / path);
406             break;
407         case PathType::DIRECTORY:
408             result = copyDirectory(from / path, to / path);
409             break;
410         default:
411             // just ignore the rest
412             result = SUCCESS;
413         }
414         if (!result) {
415             return result;
416         }
417     }
418     return SUCCESS;
419 }
420 
setWorkingDirectory(const Path & path)421 bool FileSystem::setWorkingDirectory(const Path& path) {
422     SPH_ASSERT(pathType(path).valueOr(PathType::OTHER) == PathType::DIRECTORY);
423 #ifndef SPH_WIN
424     return chdir(path.native()) == 0;
425 #else
426     return SetCurrentDirectoryW(path.native());
427 #endif
428 }
429 
getDirectoryOfExecutable()430 Expected<Path> FileSystem::getDirectoryOfExecutable() {
431 #ifndef SPH_WIN
432     char result[4096] = { '\0' };
433 #if defined(__FreeBSD__) || defined(__DragonFly__)
434     ssize_t count = readlink("/proc/curproc/file", result, sizeof(result));
435 #else
436     ssize_t count = readlink("/proc/self/exe", result, sizeof(result));
437 #endif
438     if (count != -1) {
439         Path path(String::fromUtf8(result));
440         return path.parentPath();
441     } else {
442         return makeUnexpected<Path>("Unknown error");
443     }
444 #else
445     wchar_t path[MAX_PATH];
446     if (GetModuleFileNameW(nullptr, path, MAX_PATH) != 0) {
447         return Path(path).parentPath();
448     } else {
449         return makeUnexpected<Path>(getLastErrorMessage());
450     }
451 #endif
452 }
453 
454 struct FileSystem::DirectoryIterator::DirData {
455 #ifndef SPH_WIN
456     DIR* dir = nullptr;
457     dirent* entry = nullptr;
458 #else
459     HANDLE dir = INVALID_HANDLE_VALUE;
460     WIN32_FIND_DATAW entry;
461     DWORD error = 1;
462 #endif
463 };
464 
isSpecial(const Path & path)465 static bool isSpecial(const Path& path) {
466     return path == Path(".") || path == Path("..");
467 }
468 
DirectoryIterator(DirData && dirData)469 FileSystem::DirectoryIterator::DirectoryIterator(DirData&& dirData)
470     : data(makeAuto<DirData>(std::move(dirData))) {
471     SPH_ASSERT(data);
472 #ifndef SPH_WIN
473     if (data->dir != nullptr) {
474         // find first file
475         this->operator++();
476     }
477 #else
478     // skip special paths
479     while (data->dir != INVALID_HANDLE_VALUE && !data->error && isSpecial(**this)) {
480         this->operator++();
481     }
482 #endif
483 }
484 
DirectoryIterator(const DirectoryIterator & other)485 FileSystem::DirectoryIterator::DirectoryIterator(const DirectoryIterator& other) {
486     *this = other;
487 }
488 
operator =(const DirectoryIterator & other)489 FileSystem::DirectoryIterator& FileSystem::DirectoryIterator::operator=(const DirectoryIterator& other) {
490     data = makeAuto<DirData>(*other.data);
491     return *this;
492 }
493 
494 FileSystem::DirectoryIterator::~DirectoryIterator() = default;
495 
operator ++()496 FileSystem::DirectoryIterator& FileSystem::DirectoryIterator::operator++() {
497 #ifndef SPH_WIN
498     if (data->dir == nullptr) {
499         return *this;
500     }
501     do {
502         data->entry = readdir(data->dir);
503     } while (data->entry && isSpecial(**this));
504 #else
505     if (data->dir == INVALID_HANDLE_VALUE) {
506         return *this;
507     }
508     bool result = true;
509     do {
510         result = FindNextFileW(data->dir, &data->entry);
511     } while (result && isSpecial(**this));
512 
513     if (!result) {
514         data->error = GetLastError();
515     }
516 #endif
517     return *this;
518 }
519 
operator *() const520 Path FileSystem::DirectoryIterator::operator*() const {
521 #ifndef SPH_WIN
522     SPH_ASSERT(data && data->entry);
523     return Path(String::fromUtf8(data->entry->d_name));
524 #else
525     SPH_ASSERT(data && !data->error);
526     return Path(data->entry.cFileName);
527 #endif
528 }
529 
operator ==(const DirectoryIterator & other) const530 bool FileSystem::DirectoryIterator::operator==(const DirectoryIterator& other) const {
531 #ifndef SPH_WIN
532     return (!data->entry && !other.data->entry) || data->entry == other.data->entry;
533 #else
534     return (data->error && other.data->error) || data->dir == other.data->dir;
535 #endif
536 }
537 
operator !=(const DirectoryIterator & other) const538 bool FileSystem::DirectoryIterator::operator!=(const DirectoryIterator& other) const {
539     // returns false if both are nullptr to end the loop for nonexisting dirs
540     return !(*this == other);
541 }
542 
DirectoryAdapter(const Path & directory)543 FileSystem::DirectoryAdapter::DirectoryAdapter(const Path& directory) {
544     SPH_ASSERT(pathType(directory).valueOr(PathType::OTHER) == PathType::DIRECTORY);
545     data = makeAuto<DirectoryIterator::DirData>();
546 #ifndef SPH_WIN
547     if (!pathExists(directory)) {
548         data->dir = nullptr;
549         return;
550     }
551     data->dir = opendir(directory.native());
552 #else
553     if (!pathExists(directory)) {
554         data->dir = INVALID_HANDLE_VALUE;
555         return;
556     }
557     String searchExpression = (directory / Path("*")).string();
558     data->dir = FindFirstFileW(searchExpression.toUnicode(), &data->entry);
559     if (data->dir != INVALID_HANDLE_VALUE) {
560         data->error = 0;
561     }
562 #endif
563 }
564 
~DirectoryAdapter()565 FileSystem::DirectoryAdapter::~DirectoryAdapter() {
566 #ifndef SPH_WIN
567     if (data->dir) {
568         closedir(data->dir);
569     }
570 #else
571     if (data->dir != INVALID_HANDLE_VALUE) {
572         FindClose(data->dir);
573     }
574 #endif
575 }
576 
DirectoryAdapter(DirectoryAdapter && other)577 FileSystem::DirectoryAdapter::DirectoryAdapter(DirectoryAdapter&& other) {
578     data = std::move(other.data);
579     other.data = makeAuto<DirectoryIterator::DirData>();
580 }
581 
begin() const582 FileSystem::DirectoryIterator FileSystem::DirectoryAdapter::begin() const {
583     return DirectoryIterator(DirectoryIterator::DirData(*data));
584 }
585 
end() const586 FileSystem::DirectoryIterator FileSystem::DirectoryAdapter::end() const {
587     return DirectoryIterator({});
588 }
589 
iterateDirectory(const Path & directory)590 FileSystem::DirectoryAdapter FileSystem::iterateDirectory(const Path& directory) {
591     return DirectoryAdapter(directory);
592 }
593 
getFilesInDirectory(const Path & directory)594 Array<Path> FileSystem::getFilesInDirectory(const Path& directory) {
595     Array<Path> paths;
596     for (Path path : iterateDirectory(directory)) {
597         paths.push(path);
598     }
599     return paths;
600 }
601 
602 NAMESPACE_SPH_END
603