Filesystem.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. /**
  2. * Copyright (c) 2006-2014 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. #include "common/config.h"
  21. #include <iostream>
  22. #include "common/utf8.h"
  23. #include "common/b64.h"
  24. #include "Filesystem.h"
  25. #include "SDL.h"
  26. #include <string>
  27. #ifdef LOVE_ANDROID
  28. #include <sys/stat.h>
  29. #include <sys/types.h>
  30. #include <unistd.h>
  31. #include <errno.h>
  32. #include "common/android.h"
  33. #endif
  34. namespace
  35. {
  36. size_t getDriveDelim(const std::string &input)
  37. {
  38. for (size_t i = 0; i < input.size(); ++i)
  39. if (input[i] == '/' || input[i] == '\\')
  40. return i;
  41. // Something's horribly wrong
  42. return 0;
  43. }
  44. std::string getDriveRoot(const std::string &input)
  45. {
  46. return input.substr(0, getDriveDelim(input)+1);
  47. }
  48. std::string skipDriveRoot(const std::string &input)
  49. {
  50. return input.substr(getDriveDelim(input)+1);
  51. }
  52. #ifdef LOVE_ANDROID
  53. void androidDeleteAssetGameMemory (void *ptr) {
  54. SDL_Log ("freeing memory for in-memory game.love");
  55. char *game_love_data = static_cast<char*>(ptr);
  56. delete[] game_love_data;
  57. }
  58. bool androidMountGame (const char *filename) {
  59. SDL_Log ("Trying to mount %s", filename);
  60. SDL_RWops *asset_game_file = SDL_RWFromFile(filename, "rb");
  61. if (!asset_game_file) {
  62. SDL_Log ("Could not find %s", filename);
  63. return false;
  64. }
  65. Sint64 file_size = asset_game_file->size(asset_game_file);
  66. if (file_size == 0) {
  67. SDL_Log ("Could not find valid %s. File has zero size.", filename);
  68. return false;
  69. }
  70. char *game_love_data = new char[file_size];
  71. if (!game_love_data) {
  72. SDL_Log ("Could not allocate memory for in-memory game archive");
  73. return false;
  74. }
  75. size_t bytes_copied = asset_game_file->read(asset_game_file, game_love_data, sizeof(char), (size_t) file_size);
  76. if (bytes_copied != file_size) {
  77. SDL_Log ("Only copied %d of %d bytes into in-memory game archive!", bytes_copied, file_size);
  78. delete[] game_love_data;
  79. return false;
  80. }
  81. bool result = false;
  82. if (PHYSFS_mountMemory (game_love_data, file_size, androidDeleteAssetGameMemory, "archive.zip", "/", 0)) {
  83. result = true;
  84. } else {
  85. SDL_Log ("Mounting of in-memory game archive failed!");
  86. }
  87. SDL_Log ("Mounting of in-memory game archive successful!");
  88. return result;
  89. }
  90. bool androidMountAssetGame () {
  91. return androidMountGame ("game.love");
  92. }
  93. bool androidMountSelectedGame () {
  94. return androidMountGame (love::android::getSelectedGameFile());
  95. }
  96. bool androidDirectoryExists(const char* path) {
  97. SDL_Log ("Checking directory exists for %s", path);
  98. struct stat s;
  99. int err = stat(path, &s);
  100. if (err == -1) {
  101. if (errno == ENOENT)
  102. return false;
  103. else {
  104. SDL_Log ("Error checking for directory %s errno = %d: %s", path, errno, strerror (errno));
  105. return false;
  106. }
  107. } else if(S_ISDIR(s.st_mode)) {
  108. SDL_Log ("Directory %s exists!", path);
  109. return true;
  110. }
  111. return false;
  112. }
  113. bool androidMkdir (const char* path) {
  114. int err = mkdir (path, 0770);
  115. if (err == -1) {
  116. SDL_Log ("Error: Could not create directory %s", path);
  117. return false;
  118. }
  119. return true;
  120. }
  121. bool androidCreateStorageDirectories () {
  122. std::string internal_storage_path = SDL_AndroidGetInternalStoragePath();
  123. std::string save_directory = internal_storage_path + "/save";
  124. if (!androidDirectoryExists (save_directory.c_str())) {
  125. if (!androidMkdir(save_directory.c_str()))
  126. return false;
  127. }
  128. std::string game_directory = internal_storage_path + "/game";
  129. if (!androidDirectoryExists (game_directory.c_str())) {
  130. if (!androidMkdir(game_directory.c_str()))
  131. return false;
  132. }
  133. SDL_Log ("Creating storage directories successful!");
  134. return true;
  135. }
  136. #endif
  137. }
  138. namespace love
  139. {
  140. namespace filesystem
  141. {
  142. namespace physfs
  143. {
  144. Filesystem::Filesystem()
  145. : initialized(false)
  146. , fused(false)
  147. , fusedSet(false)
  148. {
  149. }
  150. Filesystem::~Filesystem()
  151. {
  152. if (initialized)
  153. PHYSFS_deinit();
  154. }
  155. const char *Filesystem::getName() const
  156. {
  157. return "love.filesystem.physfs";
  158. }
  159. void Filesystem::init(const char *arg0)
  160. {
  161. if (!PHYSFS_init(arg0))
  162. throw Exception(PHYSFS_getLastError());
  163. #ifdef LOVE_ANDROID
  164. PHYSFS_permitSymbolicLinks (1);
  165. #endif
  166. initialized = true;
  167. }
  168. void Filesystem::setFused(bool fused)
  169. {
  170. if (fusedSet)
  171. return;
  172. this->fused = fused;
  173. fusedSet = true;
  174. }
  175. bool Filesystem::isFused() const
  176. {
  177. if (!fusedSet)
  178. return false;
  179. return fused;
  180. }
  181. bool Filesystem::setIdentity(const char *ident, bool appendToPath)
  182. {
  183. if (!initialized)
  184. return false;
  185. std::string old_save_path = save_path_full;
  186. // Store the save directory.
  187. save_identity = std::string(ident);
  188. // Generate the relative path to the game save folder.
  189. save_path_relative = std::string(LOVE_APPDATA_PREFIX LOVE_APPDATA_FOLDER LOVE_PATH_SEPARATOR) + save_identity;
  190. // Generate the full path to the game save folder.
  191. save_path_full = std::string(getAppdataDirectory()) + std::string(LOVE_PATH_SEPARATOR);
  192. if (fused)
  193. save_path_full += std::string(LOVE_APPDATA_PREFIX) + save_identity;
  194. else
  195. save_path_full += save_path_relative;
  196. #ifdef LOVE_ANDROID
  197. std::string internal_storage_path = SDL_AndroidGetInternalStoragePath();
  198. std::string save_directory = internal_storage_path + "/save";
  199. save_path_full = std::string(SDL_AndroidGetInternalStoragePath()) + std::string("/save/") + save_identity;
  200. if (androidDirectoryExists (save_path_full.c_str())) {
  201. SDL_Log ("dir exists");
  202. } else {
  203. SDL_Log ("does not exist");
  204. }
  205. if (!androidDirectoryExists (save_path_full.c_str())) {
  206. if (!androidMkdir (save_path_full.c_str())) {
  207. SDL_Log ("Error: Could not create save directory %s!", save_path_full.c_str());
  208. } else {
  209. SDL_Log ("Save directory %s successfuly created!", save_path_full.c_str());
  210. }
  211. } else {
  212. SDL_Log ("Save directory %s exists!", save_path_full.c_str());
  213. }
  214. #endif
  215. // We now have something like:
  216. // save_identity: game
  217. // save_path_relative: ./LOVE/game
  218. // save_path_full: C:\Documents and Settings\user\Application Data/LOVE/game
  219. // We don't want old read-only save paths to accumulate when we set a new
  220. // identity.
  221. if (!old_save_path.empty())
  222. PHYSFS_removeFromSearchPath(old_save_path.c_str());
  223. // Try to add the save directory to the search path.
  224. // (No error on fail, it means that the path doesn't exist).
  225. PHYSFS_addToSearchPath(save_path_full.c_str(), appendToPath);
  226. return true;
  227. }
  228. const char *Filesystem::getIdentity() const
  229. {
  230. return save_identity.c_str();
  231. }
  232. bool Filesystem::setSource(const char *source)
  233. {
  234. if (!initialized)
  235. return false;
  236. // Check whether directory is already set.
  237. if (!game_source.empty())
  238. return false;
  239. std::string new_search_path = source;
  240. #ifdef LOVE_ANDROID
  241. if (!androidCreateStorageDirectories ()) {
  242. SDL_Log ("Error creating storage directories!");
  243. }
  244. if (!androidMountSelectedGame() && !androidMountAssetGame()) {
  245. SDL_RWops *sdcard_main = SDL_RWFromFile("/sdcard/lovegame/main.lua", "rb");
  246. if (sdcard_main) {
  247. SDL_Log ("using game from /sdcard/lovegame");
  248. new_search_path = "/sdcard/lovegame";
  249. sdcard_main->close(sdcard_main);
  250. if (!PHYSFS_addToSearchPath(new_search_path.c_str(), 1)) {
  251. SDL_Log ("mounting of %s failed", new_search_path.c_str());
  252. return false;
  253. }
  254. } else {
  255. // Neither assets/game.love or /sdcard/lovegame was mounted
  256. // sucessfully, therefore simply fail.
  257. return false;
  258. }
  259. }
  260. #else
  261. // Add the directory.
  262. if (!PHYSFS_addToSearchPath(new_search_path.c_str(), 1)) {
  263. SDL_Log ("mounting of %s failed", new_search_path.c_str());
  264. return false;
  265. }
  266. #endif
  267. // Save the game source.
  268. game_source = new_search_path;
  269. return true;
  270. }
  271. const char *Filesystem::getSource() const
  272. {
  273. return game_source.c_str();
  274. }
  275. bool Filesystem::setupWriteDirectory()
  276. {
  277. if (!initialized)
  278. return false;
  279. // These must all be set.
  280. if (save_identity.empty() || save_path_full.empty() || save_path_relative.empty())
  281. return false;
  282. // Set the appdata folder as writable directory.
  283. // (We must create the save folder before mounting it).
  284. if (!PHYSFS_setWriteDir(getDriveRoot(save_path_full).c_str()))
  285. return false;
  286. // Create the save folder. (We're now "at" %APPDATA%).
  287. if (!createDirectory(skipDriveRoot(save_path_full).c_str()))
  288. {
  289. // Clear the write directory in case of error.
  290. PHYSFS_setWriteDir(0);
  291. return false;
  292. }
  293. // Set the final write directory.
  294. if (!PHYSFS_setWriteDir(save_path_full.c_str()))
  295. return false;
  296. // Add the directory. (Will not be readded if already present).
  297. if (!PHYSFS_addToSearchPath(save_path_full.c_str(), 0))
  298. {
  299. PHYSFS_setWriteDir(0); // Clear the write directory in case of error.
  300. return false;
  301. }
  302. return true;
  303. }
  304. bool Filesystem::mount(const char *archive, const char *mountpoint, bool appendToPath)
  305. {
  306. if (!initialized || !archive)
  307. return false;
  308. std::string realPath;
  309. std::string sourceBase = getSourceBaseDirectory();
  310. if (isFused() && sourceBase.compare(archive) == 0)
  311. {
  312. // Special case: if the game is fused and the archive is the source's
  313. // base directory, mount it even though it's outside of the save dir.
  314. realPath = sourceBase;
  315. }
  316. else
  317. {
  318. // Not allowed for safety reasons.
  319. if (strlen(archive) == 0 || strstr(archive, "..") || strcmp(archive, "/") == 0)
  320. return false;
  321. const char *realDir = PHYSFS_getRealDir(archive);
  322. if (!realDir)
  323. return false;
  324. realPath = realDir;
  325. // Always disallow mounting of files inside the game source, since it
  326. // won't work anyway if the game source is a zipped .love file.
  327. if (realPath.find(game_source) == 0)
  328. return false;
  329. realPath += LOVE_PATH_SEPARATOR;
  330. realPath += archive;
  331. }
  332. if (realPath.length() == 0)
  333. return false;
  334. return PHYSFS_mount(realPath.c_str(), mountpoint, appendToPath);
  335. }
  336. bool Filesystem::unmount(const char *archive)
  337. {
  338. if (!initialized || !archive)
  339. return false;
  340. std::string realPath;
  341. std::string sourceBase = getSourceBaseDirectory();
  342. if (isFused() && sourceBase.compare(archive) == 0)
  343. {
  344. // Special case: if the game is fused and the archive is the source's
  345. // base directory, unmount it even though it's outside of the save dir.
  346. realPath = sourceBase;
  347. }
  348. else
  349. {
  350. // Not allowed for safety reasons.
  351. if (strlen(archive) == 0 || strstr(archive, "..") || strcmp(archive, "/") == 0)
  352. return false;
  353. const char *realDir = PHYSFS_getRealDir(archive);
  354. if (!realDir)
  355. return false;
  356. realPath = realDir;
  357. realPath += LOVE_PATH_SEPARATOR;
  358. realPath += archive;
  359. }
  360. const char *mountPoint = PHYSFS_getMountPoint(realPath.c_str());
  361. if (!mountPoint)
  362. return false;
  363. return PHYSFS_removeFromSearchPath(realPath.c_str());
  364. }
  365. File *Filesystem::newFile(const char *filename) const
  366. {
  367. return new File(filename);
  368. }
  369. FileData *Filesystem::newFileData(void *data, unsigned int size, const char *filename) const
  370. {
  371. FileData *fd = new FileData(size, std::string(filename));
  372. // Copy the data into FileData.
  373. memcpy(fd->getData(), data, size);
  374. return fd;
  375. }
  376. FileData *Filesystem::newFileData(const char *b64, const char *filename) const
  377. {
  378. int size = strlen(b64);
  379. int outsize = 0;
  380. char *dst = b64_decode(b64, size, outsize);
  381. FileData *fd = new FileData(outsize, std::string(filename));
  382. // Copy the data into FileData.
  383. memcpy(fd->getData(), dst, outsize);
  384. delete [] dst;
  385. return fd;
  386. }
  387. const char *Filesystem::getWorkingDirectory()
  388. {
  389. if (cwd.empty())
  390. {
  391. #ifdef LOVE_WINDOWS
  392. WCHAR w_cwd[LOVE_MAX_PATH];
  393. _wgetcwd(w_cwd, LOVE_MAX_PATH);
  394. cwd = to_utf8(w_cwd);
  395. replace_char(cwd, '\\', '/');
  396. #else
  397. char *cwd_char = new char[LOVE_MAX_PATH];
  398. if (getcwd(cwd_char, LOVE_MAX_PATH))
  399. cwd = cwd_char; // if getcwd fails, cwd_char (and thus cwd) will still be empty
  400. delete [] cwd_char;
  401. #endif
  402. }
  403. return cwd.c_str();
  404. }
  405. const char *Filesystem::getUserDirectory()
  406. {
  407. return PHYSFS_getUserDir();
  408. }
  409. const char *Filesystem::getAppdataDirectory()
  410. {
  411. #ifdef LOVE_WINDOWS
  412. if (appdata.empty())
  413. {
  414. wchar_t *w_appdata = _wgetenv(L"APPDATA");
  415. appdata = to_utf8(w_appdata);
  416. replace_char(appdata, '\\', '/');
  417. }
  418. return appdata.c_str();
  419. #elif defined(LOVE_MACOSX)
  420. if (appdata.empty())
  421. {
  422. std::string udir = getUserDirectory();
  423. udir.append("/Library/Application Support");
  424. appdata = udir;
  425. }
  426. return appdata.c_str();
  427. #elif defined(LOVE_LINUX)
  428. if (appdata.empty())
  429. {
  430. char *xdgdatahome = getenv("XDG_DATA_HOME");
  431. if (!xdgdatahome)
  432. appdata = std::string(getUserDirectory()) + "/.local/share/";
  433. else
  434. appdata = xdgdatahome;
  435. }
  436. return appdata.c_str();
  437. #else
  438. return getUserDirectory();
  439. #endif
  440. }
  441. const char *Filesystem::getSaveDirectory()
  442. {
  443. return save_path_full.c_str();
  444. }
  445. std::string Filesystem::getSourceBaseDirectory() const
  446. {
  447. size_t source_len = game_source.length();
  448. if (source_len == 0)
  449. return "";
  450. // FIXME: This doesn't take into account parent and current directory
  451. // symbols (i.e. '..' and '.')
  452. #ifdef LOVE_WINDOWS
  453. // In windows, delimiters can be either '/' or '\'.
  454. size_t base_end_pos = game_source.find_last_of("/\\", source_len - 2);
  455. #else
  456. size_t base_end_pos = game_source.find_last_of('/', source_len - 2);
  457. #endif
  458. if (base_end_pos == std::string::npos)
  459. return "";
  460. // If the source is in the unix root (aka '/'), we want to keep the '/'.
  461. if (base_end_pos == 0)
  462. base_end_pos = 1;
  463. return game_source.substr(0, base_end_pos);
  464. }
  465. bool Filesystem::exists(const char *file) const
  466. {
  467. return PHYSFS_exists(file);
  468. }
  469. bool Filesystem::isDirectory(const char *file) const
  470. {
  471. return PHYSFS_isDirectory(file);
  472. }
  473. bool Filesystem::isFile(const char *file) const
  474. {
  475. return exists(file) && !isDirectory(file);
  476. }
  477. bool Filesystem::createDirectory(const char *dir)
  478. {
  479. if (PHYSFS_getWriteDir() == 0 && !setupWriteDirectory())
  480. return false;
  481. if (!PHYSFS_mkdir(dir))
  482. return false;
  483. return true;
  484. }
  485. bool Filesystem::remove(const char *file)
  486. {
  487. if (PHYSFS_getWriteDir() == 0 && !setupWriteDirectory())
  488. return false;
  489. if (!PHYSFS_delete(file))
  490. return false;
  491. return true;
  492. }
  493. Data *Filesystem::read(const char *filename, int64 size) const
  494. {
  495. File file(filename);
  496. file.open(File::READ);
  497. // close() is called in the File destructor.
  498. return file.read(size);
  499. }
  500. void Filesystem::write(const char *filename, const void *data, int64 size) const
  501. {
  502. File file(filename);
  503. file.open(File::WRITE);
  504. // close() is called in the File destructor.
  505. if (!file.write(data, size))
  506. throw love::Exception("Data could not be written.");
  507. }
  508. void Filesystem::append(const char *filename, const void *data, int64 size) const
  509. {
  510. File file(filename);
  511. file.open(File::APPEND);
  512. // close() is called in the File destructor.
  513. if (!file.write(data, size))
  514. throw love::Exception("Data could not be written.");
  515. }
  516. int Filesystem::getDirectoryItems(lua_State *L)
  517. {
  518. const char *dir = luaL_checkstring(L, 1);
  519. char **rc = PHYSFS_enumerateFiles(dir);
  520. int index = 1;
  521. lua_newtable(L);
  522. for (char **i = rc; *i != 0; i++)
  523. {
  524. lua_pushstring(L, *i);
  525. lua_rawseti(L, -2, index);
  526. index++;
  527. }
  528. PHYSFS_freeList(rc);
  529. return 1;
  530. }
  531. int Filesystem::lines_i(lua_State *L)
  532. {
  533. const int bufsize = 1024;
  534. char buf[bufsize];
  535. int linesize = 0;
  536. bool newline = false;
  537. File *file = luax_checktype<File>(L, lua_upvalueindex(1), "File", FILESYSTEM_FILE_T);
  538. // Only accept read mode at this point.
  539. if (file->getMode() != File::READ)
  540. return luaL_error(L, "File needs to stay in read mode.");
  541. int64 pos = file->tell();
  542. int64 userpos = -1;
  543. if (lua_isnoneornil(L, lua_upvalueindex(2)) == 0)
  544. {
  545. // User may have changed the file position.
  546. userpos = pos;
  547. pos = (int64) lua_tonumber(L, lua_upvalueindex(2));
  548. if (userpos != pos)
  549. file->seek(pos);
  550. }
  551. while (!newline && !file->eof())
  552. {
  553. // This 64-bit to 32-bit integer cast should be safe as it never exceeds bufsize.
  554. int read = (int) file->read(buf, bufsize);
  555. if (read < 0)
  556. return luaL_error(L, "Could not read from file.");
  557. linesize += read;
  558. for (int i = 0; i < read; i++)
  559. {
  560. if (buf[i] == '\n')
  561. {
  562. linesize -= read - i;
  563. newline = true;
  564. break;
  565. }
  566. }
  567. }
  568. if (newline || (file->eof() && linesize > 0))
  569. {
  570. if (linesize < bufsize)
  571. {
  572. // We have the line in the buffer on the stack. No 'new' and 'read' needed.
  573. lua_pushlstring(L, buf, linesize > 0 && buf[linesize - 1] == '\r' ? linesize - 1 : linesize);
  574. if (userpos < 0)
  575. file->seek(pos + linesize + 1);
  576. }
  577. else
  578. {
  579. char *str = 0;
  580. try
  581. {
  582. str = new char[linesize + 1];
  583. }
  584. catch(std::bad_alloc &)
  585. {
  586. // Can't lua_error (longjmp) in exception handlers.
  587. }
  588. if (!str)
  589. return luaL_error(L, "Out of memory.");
  590. file->seek(pos);
  591. // Read the \n anyway and save us a call to seek.
  592. if (file->read(str, linesize + 1) == -1)
  593. {
  594. delete [] str;
  595. return luaL_error(L, "Could not read from file.");
  596. }
  597. lua_pushlstring(L, str, str[linesize - 1] == '\r' ? linesize - 1 : linesize);
  598. delete [] str;
  599. }
  600. if (userpos >= 0)
  601. {
  602. // Save new position in upvalue.
  603. lua_pushnumber(L, (lua_Number)(pos + linesize + 1));
  604. lua_replace(L, lua_upvalueindex(2));
  605. file->seek(userpos);
  606. }
  607. return 1;
  608. }
  609. // EOF reached.
  610. if (userpos >= 0 && luax_toboolean(L, lua_upvalueindex(3)))
  611. file->seek(userpos);
  612. else
  613. file->close();
  614. return 0;
  615. }
  616. int64 Filesystem::getLastModified(const char *filename) const
  617. {
  618. PHYSFS_sint64 time = PHYSFS_getLastModTime(filename);
  619. if (time == -1)
  620. throw love::Exception("Could not determine file modification date.");
  621. return time;
  622. }
  623. int64 Filesystem::getSize(const char *filename) const
  624. {
  625. File file(filename);
  626. int64 size = file.getSize();
  627. return size;
  628. }
  629. } // physfs
  630. } // filesystem
  631. } // love