ResourceFilesystem.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. // Copyright (C) 2009-2023, 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/Core/CVarSet.h>
  8. #include <AnKi/Util/Tracer.h>
  9. #include <ZLib/contrib/minizip/unzip.h>
  10. #if ANKI_OS_ANDROID
  11. # include <android_native_app_glue.h>
  12. #endif
  13. namespace anki {
  14. StringCVar g_dataPathsCVar(CVarSubsystem::kResource, "DataPaths", ".",
  15. "The engine loads assets only in from these paths. Separate them with : (it's smart enough to identify drive letters in "
  16. "Windows). After a path you can add an optional | and what follows it is a number of words to include or exclude paths. "
  17. "eg. my_path|include_this,include_that,+exclude_this");
  18. static Error tokenizePath(CString path, ResourceString& actualPath, ResourceStringList& includedWords, ResourceStringList& excludedWords)
  19. {
  20. ResourceStringList tokens;
  21. tokens.splitString(path, '|');
  22. const PtrSize count = tokens.getSize();
  23. if(count != 1 && count != 2)
  24. {
  25. ANKI_RESOURCE_LOGE("Tokenization of path failed: %s", path.cstr());
  26. return Error::kUserData;
  27. }
  28. actualPath = tokens.getFront();
  29. // Further tokenization
  30. if(count == 2)
  31. {
  32. const ResourceString excludeInclude = *(tokens.getBegin() + 1);
  33. ResourceStringList tokens;
  34. tokens.splitString(excludeInclude, ',');
  35. for(const auto& word : tokens)
  36. {
  37. if(word[0] == '!')
  38. {
  39. ResourceString w(&word[1], word.getEnd());
  40. excludedWords.emplaceBack(std::move(w));
  41. }
  42. else
  43. {
  44. includedWords.emplaceBack(word);
  45. }
  46. }
  47. }
  48. return Error::kNone;
  49. }
  50. /// C resource file
  51. class CResourceFile final : public ResourceFile
  52. {
  53. public:
  54. File m_file;
  55. Error read(void* buff, PtrSize size) override
  56. {
  57. ANKI_TRACE_SCOPED_EVENT(RsrcFileRead);
  58. return m_file.read(buff, size);
  59. }
  60. Error readAllText(ResourceString& out) override
  61. {
  62. ANKI_TRACE_SCOPED_EVENT(RsrcFileRead);
  63. return m_file.readAllText(out);
  64. }
  65. Error readU32(U32& u) override
  66. {
  67. ANKI_TRACE_SCOPED_EVENT(RsrcFileRead);
  68. return m_file.readU32(u);
  69. }
  70. Error readF32(F32& f) override
  71. {
  72. ANKI_TRACE_SCOPED_EVENT(RsrcFileRead);
  73. return m_file.readF32(f);
  74. }
  75. Error seek(PtrSize offset, FileSeekOrigin origin) override
  76. {
  77. return m_file.seek(offset, origin);
  78. }
  79. PtrSize getSize() const override
  80. {
  81. return m_file.getSize();
  82. }
  83. };
  84. /// ZIP file
  85. class ZipResourceFile final : public ResourceFile
  86. {
  87. public:
  88. unzFile m_archive = nullptr;
  89. PtrSize m_size = 0;
  90. ~ZipResourceFile()
  91. {
  92. if(m_archive)
  93. {
  94. // It's open
  95. unzClose(m_archive);
  96. m_archive = nullptr;
  97. m_size = 0;
  98. }
  99. }
  100. Error open(const CString& archive, const CString& archivedFname)
  101. {
  102. // Open archive
  103. m_archive = unzOpen(&archive[0]);
  104. if(m_archive == nullptr)
  105. {
  106. ANKI_RESOURCE_LOGE("Failed to open archive");
  107. return Error::kFileAccess;
  108. }
  109. // Locate archived
  110. const int caseSensitive = 1;
  111. if(unzLocateFile(m_archive, &archivedFname[0], caseSensitive) != UNZ_OK)
  112. {
  113. ANKI_RESOURCE_LOGE("Failed to locate file in archive");
  114. return Error::kFileAccess;
  115. }
  116. // Open file
  117. if(unzOpenCurrentFile(m_archive) != UNZ_OK)
  118. {
  119. ANKI_RESOURCE_LOGE("unzOpenCurrentFile() failed");
  120. return Error::kFileAccess;
  121. }
  122. // Get size just in case
  123. unz_file_info zinfo;
  124. zinfo.uncompressed_size = 0;
  125. unzGetCurrentFileInfo(m_archive, &zinfo, nullptr, 0, nullptr, 0, nullptr, 0);
  126. m_size = zinfo.uncompressed_size;
  127. ANKI_ASSERT(m_size != 0);
  128. return Error::kNone;
  129. }
  130. void close()
  131. {
  132. if(m_archive)
  133. {
  134. unzClose(m_archive);
  135. m_archive = nullptr;
  136. m_size = 0;
  137. }
  138. }
  139. Error read(void* buff, PtrSize size) override
  140. {
  141. ANKI_TRACE_SCOPED_EVENT(RsrcFileRead);
  142. I64 readSize = unzReadCurrentFile(m_archive, buff, U32(size));
  143. if(I64(size) != readSize)
  144. {
  145. ANKI_RESOURCE_LOGE("File read failed");
  146. return Error::kFileAccess;
  147. }
  148. return Error::kNone;
  149. }
  150. Error readAllText(ResourceString& out) override
  151. {
  152. ANKI_ASSERT(m_size);
  153. out = ResourceString('?', m_size);
  154. return read(&out[0], m_size);
  155. }
  156. Error readU32(U32& u) override
  157. {
  158. // Assume machine and file have same endianness
  159. ANKI_CHECK(read(&u, sizeof(u)));
  160. return Error::kNone;
  161. }
  162. Error readF32(F32& u) override
  163. {
  164. // Assume machine and file have same endianness
  165. ANKI_CHECK(read(&u, sizeof(u)));
  166. return Error::kNone;
  167. }
  168. Error seek(PtrSize offset, FileSeekOrigin origin) override
  169. {
  170. // Rewind if needed
  171. if(origin == FileSeekOrigin::kBeginning)
  172. {
  173. if(unzCloseCurrentFile(m_archive) || unzOpenCurrentFile(m_archive))
  174. {
  175. ANKI_RESOURCE_LOGE("Rewind failed");
  176. return Error::kFunctionFailed;
  177. }
  178. }
  179. // Move forward by reading dummy data
  180. Array<char, 128> buff;
  181. while(offset != 0)
  182. {
  183. PtrSize toRead = min<PtrSize>(offset, sizeof(buff));
  184. ANKI_CHECK(read(&buff[0], toRead));
  185. offset -= toRead;
  186. }
  187. return Error::kNone;
  188. }
  189. PtrSize getSize() const override
  190. {
  191. ANKI_ASSERT(m_size > 0);
  192. return m_size;
  193. }
  194. };
  195. ResourceFilesystem::~ResourceFilesystem()
  196. {
  197. }
  198. Error ResourceFilesystem::init()
  199. {
  200. ResourceStringList paths;
  201. paths.splitString(g_dataPathsCVar.get(), ':');
  202. // Workaround the fact that : is used in drives in Windows
  203. #if ANKI_OS_WINDOWS
  204. ResourceStringList paths2;
  205. ResourceStringList::Iterator it = paths.getBegin();
  206. while(it != paths.getEnd())
  207. {
  208. const ResourceString& s = *it;
  209. ResourceStringList::Iterator it2 = it + 1;
  210. if(s.getLength() == 1 && (s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z') && it2 != paths.getEnd())
  211. {
  212. paths2.pushBackSprintf("%s:%s", s.cstr(), it2->cstr());
  213. ++it;
  214. }
  215. else
  216. {
  217. paths2.pushBack(s);
  218. }
  219. ++it;
  220. }
  221. paths.destroy();
  222. paths = std::move(paths2);
  223. #endif
  224. if(paths.getSize() < 1)
  225. {
  226. ANKI_RESOURCE_LOGE("Config option \"g_dataPathsCVar\" is empty");
  227. return Error::kUserData;
  228. }
  229. for(const auto& path : paths)
  230. {
  231. ResourceStringList includedStrings;
  232. ResourceStringList excludedStrings;
  233. ResourceString actualPath;
  234. ANKI_CHECK(tokenizePath(path, actualPath, includedStrings, excludedStrings));
  235. ANKI_CHECK(addNewPath(actualPath, includedStrings, excludedStrings));
  236. }
  237. #if ANKI_OS_ANDROID
  238. // Add the external storage
  239. ANKI_CHECK(addNewPath(g_androidApp->activity->externalDataPath, excludedStrings));
  240. #endif
  241. return Error::kNone;
  242. }
  243. Error ResourceFilesystem::addNewPath(CString filepath, const ResourceStringList& includedStrings, const ResourceStringList& excludedStrings)
  244. {
  245. ANKI_RESOURCE_LOGV("Adding new resource path: %s", filepath.cstr());
  246. U32 fileCount = 0; // Count files manually because it's slower to get that number from the list
  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. path.m_files.pushBackSprintf("%s", &filename[0]);
  304. ++fileCount;
  305. }
  306. } while(unzGoToNextFile(zfile) == UNZ_OK);
  307. unzClose(zfile);
  308. path.m_isArchive = true;
  309. }
  310. else
  311. {
  312. // It's simple directory
  313. ANKI_CHECK(walkDirectoryTree(filepath, [&](const CString& fname, Bool isDir) -> Error {
  314. if(!isDir && includePath(fname))
  315. {
  316. path.m_files.pushBackSprintf("%s", fname.cstr());
  317. ++fileCount;
  318. }
  319. return Error::kNone;
  320. }));
  321. }
  322. ANKI_ASSERT(path.m_files.getSize() == fileCount);
  323. if(fileCount == 0)
  324. {
  325. ANKI_RESOURCE_LOGW("Ignoring empty resource path: %s", &filepath[0]);
  326. }
  327. else
  328. {
  329. path.m_path.sprintf("%s", &filepath[0]);
  330. m_paths.emplaceFront(std::move(path));
  331. ANKI_RESOURCE_LOGI("Added new data path \"%s\" that contains %u files", &filepath[0], fileCount);
  332. }
  333. if(false)
  334. {
  335. for(const ResourceString& s : m_paths.getFront().m_files)
  336. {
  337. printf("%s\n", s.cstr());
  338. }
  339. }
  340. return Error::kNone;
  341. }
  342. Error ResourceFilesystem::openFile(const ResourceFilename& filename, ResourceFilePtr& filePtr)
  343. {
  344. ResourceFile* rfile;
  345. Error err = openFileInternal(filename, rfile);
  346. if(err)
  347. {
  348. ANKI_RESOURCE_LOGE("Resource file not found: %s", filename.cstr());
  349. deleteInstance(ResourceMemoryPool::getSingleton(), rfile);
  350. }
  351. else
  352. {
  353. ANKI_ASSERT(rfile);
  354. filePtr.reset(rfile);
  355. }
  356. return err;
  357. }
  358. Error ResourceFilesystem::openFileInternal(const ResourceFilename& filename, ResourceFile*& rfile)
  359. {
  360. rfile = nullptr;
  361. // Search for the fname in reverse order
  362. for(const Path& p : m_paths)
  363. {
  364. for(const ResourceString& pfname : p.m_files)
  365. {
  366. if(pfname != filename)
  367. {
  368. continue;
  369. }
  370. // Found
  371. if(p.m_isArchive)
  372. {
  373. ZipResourceFile* file = newInstance<ZipResourceFile>(ResourceMemoryPool::getSingleton());
  374. rfile = file;
  375. ANKI_CHECK(file->open(p.m_path.toCString(), filename));
  376. }
  377. else
  378. {
  379. ResourceString newFname;
  380. newFname.sprintf("%s/%s", &p.m_path[0], &filename[0]);
  381. CResourceFile* file = newInstance<CResourceFile>(ResourceMemoryPool::getSingleton());
  382. rfile = file;
  383. ANKI_CHECK(file->m_file.open(newFname, FileOpenFlag::kRead));
  384. #if 0
  385. printf("Opening asset %s\n", &newFname[0]);
  386. #endif
  387. }
  388. }
  389. if(rfile)
  390. {
  391. break;
  392. }
  393. } // end for all paths
  394. // File not found? On Win/Linux try to find it outside the resource dirs. On Android try the archive
  395. if(!rfile)
  396. {
  397. CResourceFile* file = newInstance<CResourceFile>(ResourceMemoryPool::getSingleton());
  398. rfile = file;
  399. FileOpenFlag openFlags = FileOpenFlag::kRead;
  400. #if ANKI_OS_ANDROID
  401. openFlags |= FileOpenFlag::kSpecial;
  402. #endif
  403. ANKI_CHECK(file->m_file.open(filename, openFlags));
  404. #if !ANKI_OS_ANDROID
  405. ANKI_RESOURCE_LOGW("Loading resource outside the resource paths/archives. This is only OK for tools and debugging: %s", filename.cstr());
  406. #endif
  407. }
  408. return Error::kNone;
  409. }
  410. } // end namespace anki