1/**************************************************************************/
2/* dir_access_unix.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "dir_access_unix.h"
32
33#if defined(UNIX_ENABLED)
34
35#include "core/os/memory.h"
36#include "core/os/os.h"
37#include "core/string/print_string.h"
38#include "core/templates/list.h"
39
40#include <errno.h>
41#include <stdio.h>
42#include <stdlib.h>
43#include <string.h>
44#include <sys/statvfs.h>
45
46#ifdef HAVE_MNTENT
47#include <mntent.h>
48#endif
49
50Error DirAccessUnix::list_dir_begin() {
51 list_dir_end(); //close any previous dir opening!
52
53 //char real_current_dir_name[2048]; //is this enough?!
54 //getcwd(real_current_dir_name,2048);
55 //chdir(current_path.utf8().get_data());
56 dir_stream = opendir(current_dir.utf8().get_data());
57 //chdir(real_current_dir_name);
58 if (!dir_stream) {
59 return ERR_CANT_OPEN; //error!
60 }
61
62 return OK;
63}
64
65bool DirAccessUnix::file_exists(String p_file) {
66 GLOBAL_LOCK_FUNCTION
67
68 if (p_file.is_relative_path()) {
69 p_file = current_dir.path_join(p_file);
70 }
71
72 p_file = fix_path(p_file);
73
74 struct stat flags = {};
75 bool success = (stat(p_file.utf8().get_data(), &flags) == 0);
76
77 if (success && S_ISDIR(flags.st_mode)) {
78 success = false;
79 }
80
81 return success;
82}
83
84bool DirAccessUnix::dir_exists(String p_dir) {
85 GLOBAL_LOCK_FUNCTION
86
87 if (p_dir.is_relative_path()) {
88 p_dir = get_current_dir().path_join(p_dir);
89 }
90
91 p_dir = fix_path(p_dir);
92
93 struct stat flags = {};
94 bool success = (stat(p_dir.utf8().get_data(), &flags) == 0);
95
96 return (success && S_ISDIR(flags.st_mode));
97}
98
99bool DirAccessUnix::is_readable(String p_dir) {
100 GLOBAL_LOCK_FUNCTION
101
102 if (p_dir.is_relative_path()) {
103 p_dir = get_current_dir().path_join(p_dir);
104 }
105
106 p_dir = fix_path(p_dir);
107 return (access(p_dir.utf8().get_data(), R_OK) == 0);
108}
109
110bool DirAccessUnix::is_writable(String p_dir) {
111 GLOBAL_LOCK_FUNCTION
112
113 if (p_dir.is_relative_path()) {
114 p_dir = get_current_dir().path_join(p_dir);
115 }
116
117 p_dir = fix_path(p_dir);
118 return (access(p_dir.utf8().get_data(), W_OK) == 0);
119}
120
121uint64_t DirAccessUnix::get_modified_time(String p_file) {
122 if (p_file.is_relative_path()) {
123 p_file = current_dir.path_join(p_file);
124 }
125
126 p_file = fix_path(p_file);
127
128 struct stat flags = {};
129 bool success = (stat(p_file.utf8().get_data(), &flags) == 0);
130
131 if (success) {
132 return flags.st_mtime;
133 } else {
134 ERR_FAIL_V(0);
135 }
136 return 0;
137}
138
139String DirAccessUnix::get_next() {
140 if (!dir_stream) {
141 return "";
142 }
143
144 dirent *entry = readdir(dir_stream);
145
146 if (entry == nullptr) {
147 list_dir_end();
148 return "";
149 }
150
151 String fname = fix_unicode_name(entry->d_name);
152
153 // Look at d_type to determine if the entry is a directory, unless
154 // its type is unknown (the file system does not support it) or if
155 // the type is a link, in that case we want to resolve the link to
156 // known if it points to a directory. stat() will resolve the link
157 // for us.
158 if (entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK) {
159 String f = current_dir.path_join(fname);
160
161 struct stat flags = {};
162 if (stat(f.utf8().get_data(), &flags) == 0) {
163 _cisdir = S_ISDIR(flags.st_mode);
164 } else {
165 _cisdir = false;
166 }
167 } else {
168 _cisdir = (entry->d_type == DT_DIR);
169 }
170
171 _cishidden = is_hidden(fname);
172
173 return fname;
174}
175
176bool DirAccessUnix::current_is_dir() const {
177 return _cisdir;
178}
179
180bool DirAccessUnix::current_is_hidden() const {
181 return _cishidden;
182}
183
184void DirAccessUnix::list_dir_end() {
185 if (dir_stream) {
186 closedir(dir_stream);
187 }
188 dir_stream = nullptr;
189 _cisdir = false;
190}
191
192#if defined(HAVE_MNTENT) && defined(LINUXBSD_ENABLED)
193static bool _filter_drive(struct mntent *mnt) {
194 // Ignore devices that don't point to /dev
195 if (strncmp(mnt->mnt_fsname, "/dev", 4) != 0) {
196 return false;
197 }
198
199 // Accept devices mounted at common locations
200 if (strncmp(mnt->mnt_dir, "/media", 6) == 0 ||
201 strncmp(mnt->mnt_dir, "/mnt", 4) == 0 ||
202 strncmp(mnt->mnt_dir, "/home", 5) == 0 ||
203 strncmp(mnt->mnt_dir, "/run/media", 10) == 0) {
204 return true;
205 }
206
207 // Ignore everything else
208 return false;
209}
210#endif
211
212static void _get_drives(List<String> *list) {
213 // Add root.
214 list->push_back("/");
215
216#if defined(HAVE_MNTENT) && defined(LINUXBSD_ENABLED)
217 // Check /etc/mtab for the list of mounted partitions.
218 FILE *mtab = setmntent("/etc/mtab", "r");
219 if (mtab) {
220 struct mntent mnt;
221 char strings[4096];
222
223 while (getmntent_r(mtab, &mnt, strings, sizeof(strings))) {
224 if (mnt.mnt_dir != nullptr && _filter_drive(&mnt)) {
225 // Avoid duplicates
226 String name = String::utf8(mnt.mnt_dir);
227 if (!list->find(name)) {
228 list->push_back(name);
229 }
230 }
231 }
232
233 endmntent(mtab);
234 }
235#endif
236
237 // Add $HOME.
238 const char *home = getenv("HOME");
239 if (home) {
240 // Only add if it's not a duplicate
241 String home_name = String::utf8(home);
242 if (!list->find(home_name)) {
243 list->push_back(home_name);
244 }
245
246 // Check GTK+3 bookmarks for both XDG locations (Documents, Downloads, etc.)
247 // and potential user-defined bookmarks.
248 char path[1024];
249 snprintf(path, 1024, "%s/.config/gtk-3.0/bookmarks", home);
250 FILE *fd = fopen(path, "r");
251 if (fd) {
252 char string[1024];
253 while (fgets(string, 1024, fd)) {
254 // Parse only file:// links
255 if (strncmp(string, "file://", 7) == 0) {
256 // Strip any unwanted edges on the strings and push_back if it's not a duplicate.
257 String fpath = String::utf8(string + 7).strip_edges().split_spaces()[0].uri_decode();
258 if (!list->find(fpath)) {
259 list->push_back(fpath);
260 }
261 }
262 }
263
264 fclose(fd);
265 }
266
267 // Add Desktop dir.
268 String dpath = OS::get_singleton()->get_system_dir(OS::SystemDir::SYSTEM_DIR_DESKTOP);
269 if (dpath.length() > 0 && !list->find(dpath)) {
270 list->push_back(dpath);
271 }
272 }
273
274 list->sort();
275}
276
277int DirAccessUnix::get_drive_count() {
278 List<String> list;
279 _get_drives(&list);
280
281 return list.size();
282}
283
284String DirAccessUnix::get_drive(int p_drive) {
285 List<String> list;
286 _get_drives(&list);
287
288 ERR_FAIL_INDEX_V(p_drive, list.size(), "");
289
290 return list[p_drive];
291}
292
293int DirAccessUnix::get_current_drive() {
294 int drive = 0;
295 int max_length = -1;
296 const String path = get_current_dir().to_lower();
297 for (int i = 0; i < get_drive_count(); i++) {
298 const String d = get_drive(i).to_lower();
299 if (max_length < d.length() && path.begins_with(d)) {
300 max_length = d.length();
301 drive = i;
302 }
303 }
304 return drive;
305}
306
307bool DirAccessUnix::drives_are_shortcuts() {
308 return true;
309}
310
311Error DirAccessUnix::make_dir(String p_dir) {
312 GLOBAL_LOCK_FUNCTION
313
314 if (p_dir.is_relative_path()) {
315 p_dir = get_current_dir().path_join(p_dir);
316 }
317
318 p_dir = fix_path(p_dir);
319
320 bool success = (mkdir(p_dir.utf8().get_data(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == 0);
321 int err = errno;
322
323 if (success) {
324 return OK;
325 }
326
327 if (err == EEXIST) {
328 return ERR_ALREADY_EXISTS;
329 }
330
331 return ERR_CANT_CREATE;
332}
333
334Error DirAccessUnix::change_dir(String p_dir) {
335 GLOBAL_LOCK_FUNCTION
336
337 p_dir = fix_path(p_dir);
338
339 // prev_dir is the directory we are changing out of
340 String prev_dir;
341 char real_current_dir_name[2048];
342 ERR_FAIL_COND_V(getcwd(real_current_dir_name, 2048) == nullptr, ERR_BUG);
343 if (prev_dir.parse_utf8(real_current_dir_name) != OK) {
344 prev_dir = real_current_dir_name; //no utf8, maybe latin?
345 }
346
347 // try_dir is the directory we are trying to change into
348 String try_dir = "";
349 if (p_dir.is_relative_path()) {
350 String next_dir = current_dir.path_join(p_dir);
351 next_dir = next_dir.simplify_path();
352 try_dir = next_dir;
353 } else {
354 try_dir = p_dir;
355 }
356
357 bool worked = (chdir(try_dir.utf8().get_data()) == 0); // we can only give this utf8
358 if (!worked) {
359 return ERR_INVALID_PARAMETER;
360 }
361
362 String base = _get_root_path();
363 if (!base.is_empty() && !try_dir.begins_with(base)) {
364 ERR_FAIL_COND_V(getcwd(real_current_dir_name, 2048) == nullptr, ERR_BUG);
365 String new_dir;
366 new_dir.parse_utf8(real_current_dir_name);
367
368 if (!new_dir.begins_with(base)) {
369 try_dir = current_dir; //revert
370 }
371 }
372
373 // the directory exists, so set current_dir to try_dir
374 current_dir = try_dir;
375 ERR_FAIL_COND_V(chdir(prev_dir.utf8().get_data()) != 0, ERR_BUG);
376 return OK;
377}
378
379String DirAccessUnix::get_current_dir(bool p_include_drive) const {
380 String base = _get_root_path();
381 if (!base.is_empty()) {
382 String bd = current_dir.replace_first(base, "");
383 if (bd.begins_with("/")) {
384 return _get_root_string() + bd.substr(1, bd.length());
385 } else {
386 return _get_root_string() + bd;
387 }
388 }
389 return current_dir;
390}
391
392Error DirAccessUnix::rename(String p_path, String p_new_path) {
393 if (p_path.is_relative_path()) {
394 p_path = get_current_dir().path_join(p_path);
395 }
396
397 p_path = fix_path(p_path);
398
399 if (p_new_path.is_relative_path()) {
400 p_new_path = get_current_dir().path_join(p_new_path);
401 }
402
403 p_new_path = fix_path(p_new_path);
404
405 return ::rename(p_path.utf8().get_data(), p_new_path.utf8().get_data()) == 0 ? OK : FAILED;
406}
407
408Error DirAccessUnix::remove(String p_path) {
409 if (p_path.is_relative_path()) {
410 p_path = get_current_dir().path_join(p_path);
411 }
412
413 p_path = fix_path(p_path);
414
415 struct stat flags = {};
416 if ((stat(p_path.utf8().get_data(), &flags) != 0)) {
417 return FAILED;
418 }
419
420 if (S_ISDIR(flags.st_mode)) {
421 return ::rmdir(p_path.utf8().get_data()) == 0 ? OK : FAILED;
422 } else {
423 return ::unlink(p_path.utf8().get_data()) == 0 ? OK : FAILED;
424 }
425}
426
427bool DirAccessUnix::is_link(String p_file) {
428 if (p_file.is_relative_path()) {
429 p_file = get_current_dir().path_join(p_file);
430 }
431
432 p_file = fix_path(p_file);
433
434 struct stat flags = {};
435 if ((lstat(p_file.utf8().get_data(), &flags) != 0)) {
436 return FAILED;
437 }
438
439 return S_ISLNK(flags.st_mode);
440}
441
442String DirAccessUnix::read_link(String p_file) {
443 if (p_file.is_relative_path()) {
444 p_file = get_current_dir().path_join(p_file);
445 }
446
447 p_file = fix_path(p_file);
448
449 char buf[256];
450 memset(buf, 0, 256);
451 ssize_t len = readlink(p_file.utf8().get_data(), buf, sizeof(buf));
452 String link;
453 if (len > 0) {
454 link.parse_utf8(buf, len);
455 }
456 return link;
457}
458
459Error DirAccessUnix::create_link(String p_source, String p_target) {
460 if (p_target.is_relative_path()) {
461 p_target = get_current_dir().path_join(p_target);
462 }
463
464 p_source = fix_path(p_source);
465 p_target = fix_path(p_target);
466
467 if (symlink(p_source.utf8().get_data(), p_target.utf8().get_data()) == 0) {
468 return OK;
469 } else {
470 return FAILED;
471 }
472}
473
474uint64_t DirAccessUnix::get_space_left() {
475 struct statvfs vfs;
476 if (statvfs(current_dir.utf8().get_data(), &vfs) != 0) {
477 return 0;
478 }
479
480 return (uint64_t)vfs.f_bavail * (uint64_t)vfs.f_frsize;
481}
482
483String DirAccessUnix::get_filesystem_type() const {
484 return ""; //TODO this should be implemented
485}
486
487bool DirAccessUnix::is_hidden(const String &p_name) {
488 return p_name != "." && p_name != ".." && p_name.begins_with(".");
489}
490
491DirAccessUnix::DirAccessUnix() {
492 dir_stream = nullptr;
493 _cisdir = false;
494
495 /* determine drive count */
496
497 // set current directory to an absolute path of the current directory
498 char real_current_dir_name[2048];
499 ERR_FAIL_COND(getcwd(real_current_dir_name, 2048) == nullptr);
500 if (current_dir.parse_utf8(real_current_dir_name) != OK) {
501 current_dir = real_current_dir_name;
502 }
503
504 change_dir(current_dir);
505}
506
507DirAccessUnix::~DirAccessUnix() {
508 list_dir_end();
509}
510
511#endif // UNIX_ENABLED
512