1/**
2 * Copyright (c) 2006-2023 LOVE Development Team
3 *
4 * This software is provided 'as-is', without any express or implied
5 * warranty. In no event will the authors be held liable for any damages
6 * arising from the use of this software.
7 *
8 * Permission is granted to anyone to use this software for any purpose,
9 * including commercial applications, and to alter it and redistribute it
10 * freely, subject to the following restrictions:
11 *
12 * 1. The origin of this software must not be misrepresented; you must not
13 * claim that you wrote the original software. If you use this software
14 * in a product, an acknowledgment in the product documentation would be
15 * appreciated but is not required.
16 * 2. Altered source versions must be plainly marked as such, and must not be
17 * misrepresented as being the original software.
18 * 3. This notice may not be removed or altered from any source distribution.
19 **/
20
21#include <iostream>
22#include <sstream>
23#include <algorithm>
24
25#include "common/utf8.h"
26#include "common/b64.h"
27
28#include "Filesystem.h"
29#include "File.h"
30#include "PhysfsIo.h"
31
32// PhysFS
33#include "libraries/physfs/physfs.h"
34
35// For great CWD. (Current Working Directory)
36// Using this instead of boost::filesystem which totally
37// cramped our style.
38#ifdef LOVE_WINDOWS
39# include <windows.h>
40# include <direct.h>
41#else
42# include <sys/param.h>
43# include <unistd.h>
44#endif
45
46#ifdef LOVE_IOS
47# include "common/ios.h"
48#endif
49
50#include <string>
51
52#ifdef LOVE_ANDROID
53#include <SDL.h>
54#include "common/android.h"
55#endif
56
57namespace
58{
59 size_t getDriveDelim(const std::string &input)
60 {
61 for (size_t i = 0; i < input.size(); ++i)
62 if (input[i] == '/' || input[i] == '\\')
63 return i;
64 // Something's horribly wrong
65 return 0;
66 }
67
68 std::string getDriveRoot(const std::string &input)
69 {
70 return input.substr(0, getDriveDelim(input)+1);
71 }
72
73 std::string skipDriveRoot(const std::string &input)
74 {
75 return input.substr(getDriveDelim(input)+1);
76 }
77
78 std::string normalize(const std::string &input)
79 {
80 std::stringstream out;
81 bool seenSep = false, isSep = false;
82 for (size_t i = 0; i < input.size(); ++i)
83 {
84 isSep = (input[i] == LOVE_PATH_SEPARATOR[0]);
85 if (!isSep || !seenSep)
86 out << input[i];
87 seenSep = isSep;
88 }
89
90 return out.str();
91 }
92
93}
94
95namespace love
96{
97namespace filesystem
98{
99namespace physfs
100{
101
102Filesystem::Filesystem()
103 : fused(false)
104 , fusedSet(false)
105{
106 requirePath = {"?.lua", "?/init.lua"};
107 cRequirePath = {"??"};
108}
109
110Filesystem::~Filesystem()
111{
112#ifdef LOVE_ANDROID
113 love::android::deinitializeVirtualArchive();
114#endif
115
116 if (PHYSFS_isInit())
117 PHYSFS_deinit();
118}
119
120const char *Filesystem::getName() const
121{
122 return "love.filesystem.physfs";
123}
124
125void Filesystem::init(const char *arg0)
126{
127#ifdef LOVE_ANDROID
128 // TODO: This should be a pointer to an initializeed PHYSFS_AndroidInit
129 // struct on android. But it's only used for PHYSFS_getBaseDir and
130 // PHYSFS_getPrefDir, which we don't use right now...
131 arg0 = nullptr;
132#endif
133
134 if (!PHYSFS_init(arg0))
135 throw love::Exception("Failed to initialize filesystem: %s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
136
137 // Enable symlinks by default.
138 setSymlinksEnabled(true);
139}
140
141void Filesystem::setFused(bool fused)
142{
143 if (fusedSet)
144 return;
145 this->fused = fused;
146 fusedSet = true;
147}
148
149bool Filesystem::isFused() const
150{
151 if (!fusedSet)
152 return false;
153 return fused;
154}
155
156bool Filesystem::setIdentity(const char *ident, bool appendToPath)
157{
158 if (!PHYSFS_isInit())
159 return false;
160
161 std::string old_save_path = save_path_full;
162
163 // Store the save directory.
164 save_identity = std::string(ident);
165
166 // Generate the relative path to the game save folder.
167 save_path_relative = std::string(LOVE_APPDATA_PREFIX LOVE_APPDATA_FOLDER LOVE_PATH_SEPARATOR) + save_identity;
168
169 // Generate the full path to the game save folder.
170 save_path_full = std::string(getAppdataDirectory()) + std::string(LOVE_PATH_SEPARATOR);
171 if (fused)
172 save_path_full += std::string(LOVE_APPDATA_PREFIX) + save_identity;
173 else
174 save_path_full += save_path_relative;
175
176 save_path_full = normalize(save_path_full);
177
178#ifdef LOVE_ANDROID
179 if (save_identity == "")
180 save_identity = "unnamed";
181
182 std::string storage_path;
183 if (isAndroidSaveExternal())
184 storage_path = SDL_AndroidGetExternalStoragePath();
185 else
186 storage_path = SDL_AndroidGetInternalStoragePath();
187
188 std::string save_directory = storage_path + "/save";
189
190 save_path_full = storage_path + std::string("/save/") + save_identity;
191
192 if (!love::android::directoryExists(save_path_full.c_str()) &&
193 !love::android::mkdir(save_path_full.c_str()))
194 SDL_Log("Error: Could not create save directory %s!", save_path_full.c_str());
195#endif
196
197 // We now have something like:
198 // save_identity: game
199 // save_path_relative: ./LOVE/game
200 // save_path_full: C:\Documents and Settings\user\Application Data/LOVE/game
201
202 // We don't want old read-only save paths to accumulate when we set a new
203 // identity.
204 if (!old_save_path.empty())
205 PHYSFS_unmount(old_save_path.c_str());
206
207 // Try to add the save directory to the search path.
208 // (No error on fail, it means that the path doesn't exist).
209 PHYSFS_mount(save_path_full.c_str(), nullptr, appendToPath);
210
211 // HACK: This forces setupWriteDirectory to be called the next time a file
212 // is opened for writing - otherwise it won't be called at all if it was
213 // already called at least once before.
214 PHYSFS_setWriteDir(nullptr);
215
216 return true;
217}
218
219const char *Filesystem::getIdentity() const
220{
221 return save_identity.c_str();
222}
223
224bool Filesystem::setSource(const char *source)
225{
226 if (!PHYSFS_isInit())
227 return false;
228
229 // Check whether directory is already set.
230 if (!game_source.empty())
231 return false;
232
233 std::string new_search_path = source;
234
235#ifdef LOVE_ANDROID
236 if (!love::android::createStorageDirectories())
237 SDL_Log("Error creating storage directories!");
238
239 new_search_path = "";
240
241 PHYSFS_Io *gameLoveIO;
242 bool hasFusedGame = love::android::checkFusedGame((void **) &gameLoveIO);
243 bool isAAssetMounted = false;
244
245 if (hasFusedGame)
246 {
247 if (gameLoveIO)
248 // Actually we should just be able to mount gameLoveIO, but that's experimental.
249 gameLoveIO->destroy(gameLoveIO);
250 else
251 {
252 if (!love::android::initializeVirtualArchive())
253 {
254 SDL_Log("Unable to mount AAsset: %s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
255 return false;
256 }
257
258 // See love::android::initializeVirtualArchive()
259 new_search_path = "ASET.AASSET";
260 isAAssetMounted = true;
261 }
262 }
263
264 if (!isAAssetMounted)
265 {
266 new_search_path = love::android::getSelectedGameFile();
267
268 // try mounting first, if that fails, load to memory and mount
269 if (!PHYSFS_mount(new_search_path.c_str(), nullptr, 1))
270 {
271 // PHYSFS cannot yet mount a zip file inside an .apk
272 SDL_Log("Mounting %s did not work. Loading to memory.",
273 new_search_path.c_str());
274 char* game_archive_ptr = NULL;
275 size_t game_archive_size = 0;
276 if (!love::android::loadGameArchiveToMemory(
277 new_search_path.c_str(), &game_archive_ptr,
278 &game_archive_size))
279 {
280 SDL_Log("Failure memory loading archive %s", new_search_path.c_str());
281 return false;
282 }
283 if (!PHYSFS_mountMemory(
284 game_archive_ptr, game_archive_size,
285 love::android::freeGameArchiveMemory, "archive.zip", "/", 0))
286 {
287 SDL_Log("Failure mounting in-memory archive.");
288 love::android::freeGameArchiveMemory(game_archive_ptr);
289 return false;
290 }
291 }
292 }
293#else
294 // Add the directory.
295 if (!PHYSFS_mount(new_search_path.c_str(), nullptr, 1))
296 {
297 // It's possible there is additional data at the end of the fused executable,
298 // e.g. for signed windows executables (the signature).
299 // In this case let's try a little bit harder to find the zip file.
300 // This is not used by default because I assume that the physfs IOs are probably
301 // more robust and more performant, so they should be favored, if possible.
302 auto io = StripSuffixIo::create(new_search_path);
303 if (!io->determineStrippedLength())
304 {
305 delete io;
306 return false;
307 }
308 if (!PHYSFS_mountIo(io, io->filename.c_str(), nullptr, 1))
309 {
310 // If PHYSFS_mountIo fails, io->destroy(io) is not called and we have
311 // to delete ourselves.
312 delete io;
313 return false;
314 }
315 return true;
316 }
317#endif
318
319 // Save the game source.
320 game_source = new_search_path;
321
322 return true;
323}
324
325const char *Filesystem::getSource() const
326{
327 return game_source.c_str();
328}
329
330bool Filesystem::setupWriteDirectory()
331{
332 if (!PHYSFS_isInit())
333 return false;
334
335 // These must all be set.
336 if (save_identity.empty() || save_path_full.empty() || save_path_relative.empty())
337 return false;
338
339 // We need to make sure the write directory is created. To do that, we also
340 // need to make sure all its parent directories are also created.
341 std::string temp_writedir = getDriveRoot(save_path_full);
342 std::string temp_createdir = skipDriveRoot(save_path_full);
343
344 // On some sandboxed platforms, physfs will break when its write directory
345 // is the root of the drive and it tries to create a folder (even if the
346 // folder's path is in a writable location.) If the user's home folder is
347 // in the save path, we'll try starting from there instead.
348 if (save_path_full.find(getUserDirectory()) == 0)
349 {
350 temp_writedir = getUserDirectory();
351 temp_createdir = save_path_full.substr(getUserDirectory().length());
352
353 // Strip leading '/' characters from the path we want to create.
354 size_t startpos = temp_createdir.find_first_not_of('/');
355 if (startpos != std::string::npos)
356 temp_createdir = temp_createdir.substr(startpos);
357 }
358
359 // Set either '/' or the user's home as a writable directory.
360 // (We must create the save folder before mounting it).
361 if (!PHYSFS_setWriteDir(temp_writedir.c_str()))
362 return false;
363
364 // Create the save folder. (We're now "at" either '/' or the user's home).
365 if (!createDirectory(temp_createdir.c_str()))
366 {
367 // Clear the write directory in case of error.
368 PHYSFS_setWriteDir(nullptr);
369 return false;
370 }
371
372 // Set the final write directory.
373 if (!PHYSFS_setWriteDir(save_path_full.c_str()))
374 return false;
375
376 // Add the directory. (Will not be readded if already present).
377 if (!PHYSFS_mount(save_path_full.c_str(), nullptr, 0))
378 {
379 PHYSFS_setWriteDir(nullptr); // Clear the write directory in case of error.
380 return false;
381 }
382
383 return true;
384}
385
386bool Filesystem::mount(const char *archive, const char *mountpoint, bool appendToPath)
387{
388 if (!PHYSFS_isInit() || !archive)
389 return false;
390
391 std::string realPath;
392 std::string sourceBase = getSourceBaseDirectory();
393
394 // Check whether the given archive path is in the list of allowed full paths.
395 auto it = std::find(allowedMountPaths.begin(), allowedMountPaths.end(), archive);
396
397 if (it != allowedMountPaths.end())
398 realPath = *it;
399 else if (isFused() && sourceBase.compare(archive) == 0)
400 {
401 // Special case: if the game is fused and the archive is the source's
402 // base directory, mount it even though it's outside of the save dir.
403 realPath = sourceBase;
404 }
405 else
406 {
407 // Not allowed for safety reasons.
408 if (strlen(archive) == 0 || strstr(archive, "..") || strcmp(archive, "/") == 0)
409 return false;
410
411 const char *realDir = PHYSFS_getRealDir(archive);
412 if (!realDir)
413 return false;
414
415 realPath = realDir;
416
417 // Always disallow mounting of files inside the game source, since it
418 // won't work anyway if the game source is a zipped .love file.
419 if (realPath.find(game_source) == 0)
420 return false;
421
422 realPath += LOVE_PATH_SEPARATOR;
423 realPath += archive;
424 }
425
426 if (realPath.length() == 0)
427 return false;
428
429 return PHYSFS_mount(realPath.c_str(), mountpoint, appendToPath) != 0;
430}
431
432bool Filesystem::mount(Data *data, const char *archivename, const char *mountpoint, bool appendToPath)
433{
434 if (!PHYSFS_isInit())
435 return false;
436
437 if (PHYSFS_mountMemory(data->getData(), data->getSize(), nullptr, archivename, mountpoint, appendToPath) != 0)
438 {
439 mountedData[archivename] = data;
440 return true;
441 }
442
443 return false;
444}
445
446bool Filesystem::unmount(const char *archive)
447{
448 if (!PHYSFS_isInit() || !archive)
449 return false;
450
451 auto datait = mountedData.find(archive);
452
453 if (datait != mountedData.end() && PHYSFS_unmount(archive) != 0)
454 {
455 mountedData.erase(datait);
456 return true;
457 }
458
459 std::string realPath;
460 std::string sourceBase = getSourceBaseDirectory();
461
462 // Check whether the given archive path is in the list of allowed full paths.
463 auto it = std::find(allowedMountPaths.begin(), allowedMountPaths.end(), archive);
464
465 if (it != allowedMountPaths.end())
466 realPath = *it;
467 else if (isFused() && sourceBase.compare(archive) == 0)
468 {
469 // Special case: if the game is fused and the archive is the source's
470 // base directory, unmount it even though it's outside of the save dir.
471 realPath = sourceBase;
472 }
473 else
474 {
475 // Not allowed for safety reasons.
476 if (strlen(archive) == 0 || strstr(archive, "..") || strcmp(archive, "/") == 0)
477 return false;
478
479 const char *realDir = PHYSFS_getRealDir(archive);
480 if (!realDir)
481 return false;
482
483 realPath = realDir;
484 realPath += LOVE_PATH_SEPARATOR;
485 realPath += archive;
486 }
487
488 const char *mountPoint = PHYSFS_getMountPoint(realPath.c_str());
489 if (!mountPoint)
490 return false;
491
492 return PHYSFS_unmount(realPath.c_str()) != 0;
493}
494
495bool Filesystem::unmount(Data *data)
496{
497 for (const auto &datapair : mountedData)
498 {
499 if (datapair.second.get() == data)
500 {
501 std::string archive = datapair.first;
502 return unmount(archive.c_str());
503 }
504 }
505
506 return false;
507}
508
509love::filesystem::File *Filesystem::newFile(const char *filename) const
510{
511 return new File(filename);
512}
513
514const char *Filesystem::getWorkingDirectory()
515{
516 if (cwd.empty())
517 {
518#ifdef LOVE_WINDOWS
519
520 WCHAR w_cwd[LOVE_MAX_PATH];
521 _wgetcwd(w_cwd, LOVE_MAX_PATH);
522 cwd = to_utf8(w_cwd);
523 replace_char(cwd, '\\', '/');
524#else
525 char *cwd_char = new char[LOVE_MAX_PATH];
526
527 if (getcwd(cwd_char, LOVE_MAX_PATH))
528 cwd = cwd_char; // if getcwd fails, cwd_char (and thus cwd) will still be empty
529
530 delete [] cwd_char;
531#endif
532 }
533
534 return cwd.c_str();
535}
536
537std::string Filesystem::getUserDirectory()
538{
539#ifdef LOVE_IOS
540 // PHYSFS_getUserDir doesn't give exactly the path we want on iOS.
541 static std::string userDir = normalize(love::ios::getHomeDirectory());
542#else
543 static std::string userDir = normalize(PHYSFS_getUserDir());
544#endif
545
546 return userDir;
547}
548
549std::string Filesystem::getAppdataDirectory()
550{
551 if (appdata.empty())
552 {
553#ifdef LOVE_WINDOWS_UWP
554 appdata = getUserDirectory();
555#elif defined(LOVE_WINDOWS)
556 wchar_t *w_appdata = _wgetenv(L"APPDATA");
557 appdata = to_utf8(w_appdata);
558 replace_char(appdata, '\\', '/');
559#elif defined(LOVE_MACOSX)
560 std::string udir = getUserDirectory();
561 udir.append("/Library/Application Support");
562 appdata = normalize(udir);
563#elif defined(LOVE_IOS)
564 appdata = normalize(love::ios::getAppdataDirectory());
565#elif defined(LOVE_LINUX)
566 char *xdgdatahome = getenv("XDG_DATA_HOME");
567 if (!xdgdatahome)
568 appdata = normalize(std::string(getUserDirectory()) + "/.local/share/");
569 else
570 appdata = xdgdatahome;
571#else
572 appdata = getUserDirectory();
573#endif
574 }
575 return appdata;
576}
577
578
579const char *Filesystem::getSaveDirectory()
580{
581 return save_path_full.c_str();
582}
583
584std::string Filesystem::getSourceBaseDirectory() const
585{
586 size_t source_len = game_source.length();
587
588 if (source_len == 0)
589 return "";
590
591 // FIXME: This doesn't take into account parent and current directory
592 // symbols (i.e. '..' and '.')
593#ifdef LOVE_WINDOWS
594 // In windows, delimiters can be either '/' or '\'.
595 size_t base_end_pos = game_source.find_last_of("/\\", source_len - 2);
596#else
597 size_t base_end_pos = game_source.find_last_of('/', source_len - 2);
598#endif
599
600 if (base_end_pos == std::string::npos)
601 return "";
602
603 // If the source is in the unix root (aka '/'), we want to keep the '/'.
604 if (base_end_pos == 0)
605 base_end_pos = 1;
606
607 return game_source.substr(0, base_end_pos);
608}
609
610std::string Filesystem::getRealDirectory(const char *filename) const
611{
612 if (!PHYSFS_isInit())
613 throw love::Exception("PhysFS is not initialized.");
614
615 const char *dir = PHYSFS_getRealDir(filename);
616
617 if (dir == nullptr)
618 throw love::Exception("File does not exist on disk.");
619
620 return std::string(dir);
621}
622
623bool Filesystem::getInfo(const char *filepath, Info &info) const
624{
625 if (!PHYSFS_isInit())
626 return false;
627
628 PHYSFS_Stat stat = {};
629 if (!PHYSFS_stat(filepath, &stat))
630 return false;
631
632 info.size = (int64) stat.filesize;
633 info.modtime = (int64) stat.modtime;
634
635 if (stat.filetype == PHYSFS_FILETYPE_REGULAR)
636 info.type = FILETYPE_FILE;
637 else if (stat.filetype == PHYSFS_FILETYPE_DIRECTORY)
638 info.type = FILETYPE_DIRECTORY;
639 else if (stat.filetype == PHYSFS_FILETYPE_SYMLINK)
640 info.type = FILETYPE_SYMLINK;
641 else
642 info.type = FILETYPE_OTHER;
643
644 return true;
645}
646
647bool Filesystem::createDirectory(const char *dir)
648{
649 if (!PHYSFS_isInit())
650 return false;
651
652 if (PHYSFS_getWriteDir() == 0 && !setupWriteDirectory())
653 return false;
654
655 if (!PHYSFS_mkdir(dir))
656 return false;
657
658 return true;
659}
660
661bool Filesystem::remove(const char *file)
662{
663 if (!PHYSFS_isInit())
664 return false;
665
666 if (PHYSFS_getWriteDir() == 0 && !setupWriteDirectory())
667 return false;
668
669 if (!PHYSFS_delete(file))
670 return false;
671
672 return true;
673}
674
675FileData *Filesystem::read(const char *filename, int64 size) const
676{
677 File file(filename);
678
679 file.open(File::MODE_READ);
680
681 // close() is called in the File destructor.
682 return file.read(size);
683}
684
685void Filesystem::write(const char *filename, const void *data, int64 size) const
686{
687 File file(filename);
688
689 file.open(File::MODE_WRITE);
690
691 // close() is called in the File destructor.
692 if (!file.write(data, size))
693 throw love::Exception("Data could not be written.");
694}
695
696void Filesystem::append(const char *filename, const void *data, int64 size) const
697{
698 File file(filename);
699
700 file.open(File::MODE_APPEND);
701
702 // close() is called in the File destructor.
703 if (!file.write(data, size))
704 throw love::Exception("Data could not be written.");
705}
706
707void Filesystem::getDirectoryItems(const char *dir, std::vector<std::string> &items)
708{
709 if (!PHYSFS_isInit())
710 return;
711
712 char **rc = PHYSFS_enumerateFiles(dir);
713
714 if (rc == nullptr)
715 return;
716
717 for (char **i = rc; *i != 0; i++)
718 items.push_back(*i);
719
720 PHYSFS_freeList(rc);
721}
722
723void Filesystem::setSymlinksEnabled(bool enable)
724{
725 if (!PHYSFS_isInit())
726 return;
727
728 PHYSFS_permitSymbolicLinks(enable ? 1 : 0);
729}
730
731bool Filesystem::areSymlinksEnabled() const
732{
733 if (!PHYSFS_isInit())
734 return false;
735
736 return PHYSFS_symbolicLinksPermitted() != 0;
737}
738
739std::vector<std::string> &Filesystem::getRequirePath()
740{
741 return requirePath;
742}
743
744std::vector<std::string> &Filesystem::getCRequirePath()
745{
746 return cRequirePath;
747}
748
749void Filesystem::allowMountingForPath(const std::string &path)
750{
751 if (std::find(allowedMountPaths.begin(), allowedMountPaths.end(), path) == allowedMountPaths.end())
752 allowedMountPaths.push_back(path);
753}
754
755} // physfs
756} // filesystem
757} // love
758