ResourceFilesystem.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. // Copyright (C) 2009-present, Panagiotis Christopoulos Charitos and contributors.
  2. // All rights reserved.
  3. // Code licensed under the BSD License.
  4. // http://www.anki3d.org/LICENSE
  5. #include <AnKi/Resource/ResourceFilesystem.h>
  6. #include <AnKi/Util/Filesystem.h>
  7. #include <AnKi/Util/Tracer.h>
  8. #include <ZLib/contrib/minizip/unzip.h>
  9. #if ANKI_OS_ANDROID
  10. # include <android_native_app_glue.h>
  11. #endif
  12. #include <filesystem>
  13. namespace anki {
  14. static Error tokenizePath(CString path, ResourceString& actualPath, ResourceStringList& includedWords, ResourceStringList& excludedWords)
  15. {
  16. ResourceStringList tokens;
  17. tokens.splitString(path, '|');
  18. const PtrSize count = tokens.getSize();
  19. if(count != 1 && count != 2)
  20. {
  21. ANKI_RESOURCE_LOGE("Tokenization of path failed: %s", path.cstr());
  22. return Error::kUserData;
  23. }
  24. actualPath = tokens.getFront();
  25. // Further tokenization
  26. if(count == 2)
  27. {
  28. const ResourceString excludeInclude = *(tokens.getBegin() + 1);
  29. ResourceStringList tokens;
  30. tokens.splitString(excludeInclude, ',');
  31. for(const auto& word : tokens)
  32. {
  33. if(word[0] == '!')
  34. {
  35. ResourceString w(&word[1], word.getEnd());
  36. excludedWords.emplaceBack(std::move(w));
  37. }
  38. else
  39. {
  40. includedWords.emplaceBack(word);
  41. }
  42. }
  43. }
  44. return Error::kNone;
  45. }
  46. /// C resource file
  47. class CResourceFile final : public ResourceFile
  48. {
  49. public:
  50. File m_file;
  51. Error read(void* buff, PtrSize size) override
  52. {
  53. ANKI_TRACE_SCOPED_EVENT(RsrcFileRead);
  54. return m_file.read(buff, size);
  55. }
  56. Error readAllText(ResourceString& out) override
  57. {
  58. ANKI_TRACE_SCOPED_EVENT(RsrcFileRead);
  59. return m_file.readAllText(out);
  60. }
  61. Error readU32(U32& u) override
  62. {
  63. ANKI_TRACE_SCOPED_EVENT(RsrcFileRead);
  64. return m_file.readU32(u);
  65. }
  66. Error readF32(F32& f) override
  67. {
  68. ANKI_TRACE_SCOPED_EVENT(RsrcFileRead);
  69. return m_file.readF32(f);
  70. }
  71. Error seek(PtrSize offset, FileSeekOrigin origin) override
  72. {
  73. return m_file.seek(offset, origin);
  74. }
  75. PtrSize getSize() const override
  76. {
  77. return m_file.getSize();
  78. }
  79. };
  80. /// ZIP file
  81. class ZipResourceFile final : public ResourceFile
  82. {
  83. public:
  84. unzFile m_archive = nullptr;
  85. PtrSize m_size = 0;
  86. ~ZipResourceFile()
  87. {
  88. if(m_archive)
  89. {
  90. // It's open
  91. unzClose(m_archive);
  92. m_archive = nullptr;
  93. m_size = 0;
  94. }
  95. }
  96. Error open(const CString& archive, const CString& archivedFname)
  97. {
  98. // Open archive
  99. m_archive = unzOpen(&archive[0]);
  100. if(m_archive == nullptr)
  101. {
  102. ANKI_RESOURCE_LOGE("Failed to open archive");
  103. return Error::kFileAccess;
  104. }
  105. // Locate archived
  106. const int caseSensitive = 1;
  107. if(unzLocateFile(m_archive, &archivedFname[0], caseSensitive) != UNZ_OK)
  108. {
  109. ANKI_RESOURCE_LOGE("Failed to locate file in archive");
  110. return Error::kFileAccess;
  111. }
  112. // Open file
  113. if(unzOpenCurrentFile(m_archive) != UNZ_OK)
  114. {
  115. ANKI_RESOURCE_LOGE("unzOpenCurrentFile() failed");
  116. return Error::kFileAccess;
  117. }
  118. // Get size just in case
  119. unz_file_info zinfo;
  120. zinfo.uncompressed_size = 0;
  121. unzGetCurrentFileInfo(m_archive, &zinfo, nullptr, 0, nullptr, 0, nullptr, 0);
  122. m_size = zinfo.uncompressed_size;
  123. ANKI_ASSERT(m_size != 0);
  124. return Error::kNone;
  125. }
  126. void close()
  127. {
  128. if(m_archive)
  129. {
  130. unzClose(m_archive);
  131. m_archive = nullptr;
  132. m_size = 0;
  133. }
  134. }
  135. Error read(void* buff, PtrSize size) override
  136. {
  137. ANKI_TRACE_SCOPED_EVENT(RsrcFileRead);
  138. I64 readSize = unzReadCurrentFile(m_archive, buff, U32(size));
  139. if(I64(size) != readSize)
  140. {
  141. ANKI_RESOURCE_LOGE("File read failed");
  142. return Error::kFileAccess;
  143. }
  144. return Error::kNone;
  145. }
  146. Error readAllText(ResourceString& out) override
  147. {
  148. ANKI_ASSERT(m_size);
  149. out = ResourceString('?', m_size);
  150. return read(&out[0], m_size);
  151. }
  152. Error readU32(U32& u) override
  153. {
  154. // Assume machine and file have same endianness
  155. ANKI_CHECK(read(&u, sizeof(u)));
  156. return Error::kNone;
  157. }
  158. Error readF32(F32& u) override
  159. {
  160. // Assume machine and file have same endianness
  161. ANKI_CHECK(read(&u, sizeof(u)));
  162. return Error::kNone;
  163. }
  164. Error seek(PtrSize offset, FileSeekOrigin origin) override
  165. {
  166. // Rewind if needed
  167. if(origin == FileSeekOrigin::kBeginning)
  168. {
  169. if(unzCloseCurrentFile(m_archive) || unzOpenCurrentFile(m_archive))
  170. {
  171. ANKI_RESOURCE_LOGE("Rewind failed");
  172. return Error::kFunctionFailed;
  173. }
  174. }
  175. // Move forward by reading dummy data
  176. Array<char, 128> buff;
  177. while(offset != 0)
  178. {
  179. PtrSize toRead = min<PtrSize>(offset, sizeof(buff));
  180. ANKI_CHECK(read(&buff[0], toRead));
  181. offset -= toRead;
  182. }
  183. return Error::kNone;
  184. }
  185. PtrSize getSize() const override
  186. {
  187. ANKI_ASSERT(m_size > 0);
  188. return m_size;
  189. }
  190. };
  191. ResourceFilesystem::~ResourceFilesystem()
  192. {
  193. }
  194. Error ResourceFilesystem::init()
  195. {
  196. ResourceStringList paths;
  197. paths.splitString(g_cvarRsrcDataPaths, ':');
  198. // Workaround the fact that : is used in drives in Windows
  199. #if ANKI_OS_WINDOWS
  200. ResourceStringList paths2;
  201. ResourceStringList::Iterator it = paths.getBegin();
  202. while(it != paths.getEnd())
  203. {
  204. const ResourceString& s = *it;
  205. ResourceStringList::Iterator it2 = it + 1;
  206. if(s.getLength() == 1 && (s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z') && it2 != paths.getEnd())
  207. {
  208. paths2.pushBackSprintf("%s:%s", s.cstr(), it2->cstr());
  209. ++it;
  210. }
  211. else
  212. {
  213. paths2.pushBack(s);
  214. }
  215. ++it;
  216. }
  217. paths.destroy();
  218. paths = std::move(paths2);
  219. #endif
  220. if(paths.getSize() < 1)
  221. {
  222. ANKI_RESOURCE_LOGE("Config option \"g_cvarRsrcDataPaths\" is empty");
  223. return Error::kUserData;
  224. }
  225. ANKI_RESOURCE_LOGI("%s value: %s", g_cvarRsrcDataPaths.getName().cstr(), CString(g_cvarRsrcDataPaths).cstr());
  226. for(const auto& path : paths)
  227. {
  228. ResourceStringList includedStrings;
  229. ResourceStringList excludedStrings;
  230. ResourceString actualPath;
  231. ANKI_CHECK(tokenizePath(path, actualPath, includedStrings, excludedStrings));
  232. ANKI_CHECK(addNewPath(actualPath, includedStrings, excludedStrings));
  233. }
  234. #if ANKI_OS_ANDROID
  235. // Add the external storage
  236. ANKI_CHECK(addNewPath(g_androidApp->activity->externalDataPath, {}, {}));
  237. // ...and then the apk assets
  238. ANKI_CHECK(addNewPath(".apk assets", {}, {}));
  239. #endif
  240. return Error::kNone;
  241. }
  242. Error ResourceFilesystem::addNewPath(CString filepath, const ResourceStringList& includedStrings, const ResourceStringList& excludedStrings)
  243. {
  244. ANKI_RESOURCE_LOGV("Adding new resource path: %s", filepath.cstr());
  245. U32 fileCount = 0; // Count files manually because it's slower to get that number from the list
  246. ResourceStringList filenameList;
  247. constexpr CString extension(".ankizip");
  248. auto includePath = [&](CString p) -> Bool {
  249. for(const ResourceString& s : excludedStrings)
  250. {
  251. const Bool found = p.find(s) != CString::kNpos;
  252. if(found)
  253. {
  254. return false;
  255. }
  256. }
  257. if(!includedStrings.isEmpty())
  258. {
  259. for(const ResourceString& s : includedStrings)
  260. {
  261. const Bool found = p.find(s) != CString::kNpos;
  262. if(found)
  263. {
  264. return true;
  265. }
  266. }
  267. return false;
  268. }
  269. return true;
  270. };
  271. PtrSize pos;
  272. Path path;
  273. if((pos = filepath.find(extension)) != CString::kNpos && pos == filepath.getLength() - extension.getLength())
  274. {
  275. // It's an archive
  276. // Open
  277. unzFile zfile = unzOpen(&filepath[0]);
  278. if(!zfile)
  279. {
  280. ANKI_RESOURCE_LOGE("Failed to open archive");
  281. return Error::kFileAccess;
  282. }
  283. // List files
  284. if(unzGoToFirstFile(zfile) != UNZ_OK)
  285. {
  286. unzClose(zfile);
  287. ANKI_RESOURCE_LOGE("unzGoToFirstFile() failed. Empty archive?");
  288. return Error::kFileAccess;
  289. }
  290. do
  291. {
  292. Array<Char, 1024> filename;
  293. unz_file_info info;
  294. if(unzGetCurrentFileInfo(zfile, &info, &filename[0], filename.getSize(), nullptr, 0, nullptr, 0) != UNZ_OK)
  295. {
  296. unzClose(zfile);
  297. ANKI_RESOURCE_LOGE("unzGetCurrentFileInfo() failed");
  298. return Error::kFileAccess;
  299. }
  300. const Bool itsADir = info.uncompressed_size == 0;
  301. if(!itsADir && includePath(&filename[0]))
  302. {
  303. filenameList.pushBack(filename.getBegin());
  304. ++fileCount;
  305. }
  306. } while(unzGoToNextFile(zfile) == UNZ_OK);
  307. unzClose(zfile);
  308. path.m_isArchive = true;
  309. }
  310. #if ANKI_OS_ANDROID
  311. else if(filepath == ".apk assets")
  312. {
  313. File dirStructureFile;
  314. ANKI_CHECK(dirStructureFile.open("DirStructure.txt", FileOpenFlag::kRead | FileOpenFlag::kSpecial));
  315. ResourceString fileTxt;
  316. ANKI_CHECK(dirStructureFile.readAllText(fileTxt));
  317. ResourceStringList lines;
  318. lines.splitString(fileTxt, '\n');
  319. for(const auto& line : lines)
  320. {
  321. if(includePath(line))
  322. {
  323. filenameList.pushBack(line);
  324. ++fileCount;
  325. }
  326. }
  327. path.m_isSpecial = true;
  328. }
  329. #endif
  330. else
  331. {
  332. // It's simple directory
  333. ANKI_CHECK(walkDirectoryTree(filepath, [&](WalkDirectoryArgs& args) -> Error {
  334. if(!args.m_isDirectory && includePath(args.m_path))
  335. {
  336. filenameList.pushBack(args.m_path);
  337. ++fileCount;
  338. }
  339. return Error::kNone;
  340. }));
  341. }
  342. ANKI_ASSERT(filenameList.getSize() == fileCount);
  343. if(fileCount == 0)
  344. {
  345. ANKI_RESOURCE_LOGW("Ignoring empty resource path: %s", &filepath[0]);
  346. }
  347. else
  348. {
  349. path.m_path = filepath;
  350. path.m_files.resize(fileCount);
  351. U32 count = 0;
  352. for(const ResourceString& str : filenameList)
  353. {
  354. path.m_files[count].m_filename = str;
  355. path.m_files[count].m_filenameHash = str.computeHash();
  356. ++count;
  357. }
  358. m_paths.emplaceFront(std::move(path));
  359. ANKI_RESOURCE_LOGI("Added new data path \"%s\" that contains %u files", &filepath[0], fileCount);
  360. }
  361. if(false && m_paths.getSize())
  362. {
  363. for(const File& s : m_paths.getFront().m_files)
  364. {
  365. printf("%s\n", s.m_filename.cstr());
  366. }
  367. }
  368. return Error::kNone;
  369. }
  370. Error ResourceFilesystem::openFile(ResourceFilename filename, ResourceFilePtr& filePtr) const
  371. {
  372. ResourceFile* rfile;
  373. Error err = openFileInternal(filename, rfile);
  374. if(err)
  375. {
  376. ANKI_RESOURCE_LOGE("Resource file not found: %s", filename.cstr());
  377. deleteInstance(ResourceMemoryPool::getSingleton(), rfile);
  378. }
  379. else
  380. {
  381. ANKI_ASSERT(rfile);
  382. filePtr.reset(rfile);
  383. }
  384. return err;
  385. }
  386. Error ResourceFilesystem::openFileInternal(const ResourceFilename& filename, ResourceFile*& rfile) const
  387. {
  388. ANKI_RESOURCE_LOGV("Opening resource file: %s", filename.cstr());
  389. rfile = nullptr;
  390. const U64 filenameHash = filename.computeHash();
  391. // Search for the fname in reverse order
  392. for(const Path& p : m_paths)
  393. {
  394. for(const File& fsfile : p.m_files)
  395. {
  396. if(filenameHash != fsfile.m_filenameHash)
  397. {
  398. continue;
  399. }
  400. CString pfname = fsfile.m_filename;
  401. ANKI_ASSERT(pfname == filename);
  402. // Found
  403. if(p.m_isArchive)
  404. {
  405. ZipResourceFile* file = newInstance<ZipResourceFile>(ResourceMemoryPool::getSingleton());
  406. rfile = file;
  407. ANKI_CHECK(file->open(p.m_path.toCString(), filename));
  408. }
  409. else
  410. {
  411. ResourceString newFname;
  412. if(!p.m_isSpecial)
  413. {
  414. newFname.sprintf("%s/%s", p.m_path.cstr(), filename.cstr());
  415. }
  416. else
  417. {
  418. newFname = filename;
  419. }
  420. CResourceFile* file = newInstance<CResourceFile>(ResourceMemoryPool::getSingleton());
  421. rfile = file;
  422. FileOpenFlag openFlags = FileOpenFlag::kRead;
  423. if(p.m_isSpecial)
  424. {
  425. openFlags |= FileOpenFlag::kSpecial;
  426. }
  427. ANKI_CHECK(file->m_file.open(newFname, openFlags));
  428. #if 0
  429. printf("Opening asset %s, update time %llu\n", newFname.cstr(), fsfile.m_fileUpdateTime);
  430. #endif
  431. }
  432. }
  433. if(rfile)
  434. {
  435. break;
  436. }
  437. } // end for all paths
  438. #if !ANKI_OS_ANDROID
  439. // File not found? On Win/Linux try to find it outside the resource dirs
  440. if(!rfile)
  441. {
  442. CResourceFile* file = newInstance<CResourceFile>(ResourceMemoryPool::getSingleton());
  443. rfile = file;
  444. ANKI_CHECK(file->m_file.open(filename, FileOpenFlag::kRead));
  445. ANKI_RESOURCE_LOGW("Loading resource outside the resource paths/archives. This is only OK for tools and debugging: %s", filename.cstr());
  446. }
  447. #else
  448. if(!rfile)
  449. {
  450. ANKI_RESOURCE_LOGE("Couldn't find file: %s", filename.cstr());
  451. return Error::kFileNotFound;
  452. }
  453. #endif
  454. return Error::kNone;
  455. }
  456. ResourceString ResourceFilesystem::getFileFullPath(ResourceFilename filename) const
  457. {
  458. ResourceString out;
  459. const U64 filenameHash = filename.computeHash();
  460. Bool found = false;
  461. for(const Path& p : m_paths)
  462. {
  463. for(const File& fsfile : p.m_files)
  464. {
  465. if(filenameHash != fsfile.m_filenameHash)
  466. {
  467. continue;
  468. }
  469. CString pfname = fsfile.m_filename;
  470. ANKI_ASSERT(pfname == filename);
  471. if(!p.m_isArchive && !p.m_isSpecial)
  472. {
  473. out.sprintf("%s/%s", p.m_path.cstr(), fsfile.m_filename.cstr());
  474. found = true;
  475. break;
  476. }
  477. }
  478. if(found)
  479. {
  480. break;
  481. }
  482. }
  483. ANKI_ASSERT(found);
  484. return out;
  485. }
  486. U64 ResourceFilesystem::getFileUpdateTime(ResourceFilename filename) const
  487. {
  488. const ResourceString fullFilename = getFileFullPath(filename);
  489. const std::filesystem::path stdpath = fullFilename.cstr();
  490. ANKI_ASSERT(std::filesystem::exists(stdpath));
  491. const auto timeOfUpdate = std::filesystem::last_write_time(stdpath);
  492. return std::chrono::time_point_cast<std::chrono::milliseconds>(timeOfUpdate).time_since_epoch().count();
  493. }
  494. } // end namespace anki