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