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 | |
50 | Error 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 | |
65 | bool 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 | |
84 | bool 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 | |
99 | bool 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 | |
110 | bool 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 | |
121 | uint64_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 | |
139 | String 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 | |
176 | bool DirAccessUnix::current_is_dir() const { |
177 | return _cisdir; |
178 | } |
179 | |
180 | bool DirAccessUnix::current_is_hidden() const { |
181 | return _cishidden; |
182 | } |
183 | |
184 | void 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) |
193 | static 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 | |
212 | static 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 | |
277 | int DirAccessUnix::get_drive_count() { |
278 | List<String> list; |
279 | _get_drives(&list); |
280 | |
281 | return list.size(); |
282 | } |
283 | |
284 | String 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 | |
293 | int 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 | |
307 | bool DirAccessUnix::drives_are_shortcuts() { |
308 | return true; |
309 | } |
310 | |
311 | Error 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 | |
334 | Error 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 | |
379 | String 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 | |
392 | Error 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 | |
408 | Error 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 | |
427 | bool 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 | |
442 | String 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 | |
459 | Error 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 | |
474 | uint64_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 | |
483 | String DirAccessUnix::get_filesystem_type() const { |
484 | return "" ; //TODO this should be implemented |
485 | } |
486 | |
487 | bool DirAccessUnix::is_hidden(const String &p_name) { |
488 | return p_name != "." && p_name != ".." && p_name.begins_with("." ); |
489 | } |
490 | |
491 | DirAccessUnix::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 | |
507 | DirAccessUnix::~DirAccessUnix() { |
508 | list_dir_end(); |
509 | } |
510 | |
511 | #endif // UNIX_ENABLED |
512 | |