1 /*
2 * This file is part of EasyRPG Player.
3 *
4 * EasyRPG Player is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * EasyRPG Player is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with EasyRPG Player. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "directory_tree.h"
19 #include "filefinder.h"
20 #include "filesystem.h"
21 #include "output.h"
22 #include "platform.h"
23 #include "player.h"
24 #include <lcf/reader_util.h>
25
26 #ifdef EP_DEBUG_DIRECTORYTREE
27 template <typename... Args>
DebugLog(const char * fmt,Args &&...args)28 static void DebugLog(const char* fmt, Args&&... args) {
29 Output::Debug(fmt, std::forward<Args>(args)...);
30 }
31 #else
32 template <typename... Args>
DebugLog(const char *,Args &&...)33 static void DebugLog(const char*, Args&&...) {}
34 #endif
35
36 namespace {
make_key(StringView n)37 std::string make_key(StringView n) {
38 return lcf::ReaderUtil::Normalize(n);
39 };
40 }
41
Create()42 std::unique_ptr<DirectoryTree> DirectoryTree::Create() {
43 return std::make_unique<DirectoryTree>();
44 }
45
Create(Filesystem & fs)46 std::unique_ptr<DirectoryTree> DirectoryTree::Create(Filesystem& fs) {
47 std::unique_ptr<DirectoryTree> tree = std::make_unique<DirectoryTree>();
48 tree->fs = &fs;
49
50 return tree;
51 }
52
ListDirectory(StringView path) const53 DirectoryTree::DirectoryListType* DirectoryTree::ListDirectory(StringView path) const {
54 std::vector<Entry> entries;
55 std::string fs_path = ToString(path);
56
57 DebugLog("ListDirectory: {}", fs_path);
58
59 auto dir_key = make_key(fs_path);
60
61 auto dir_it = dir_cache.find(dir_key);
62 if (dir_it != dir_cache.end()) {
63 // Already cached
64 DebugLog("ListDirectory Cache Hit: {}", dir_key);
65 auto file_it = fs_cache.find(dir_key);
66 assert(file_it != fs_cache.end());
67 return &file_it->second;
68 }
69
70 assert(fs_cache.find(dir_key) == fs_cache.end());
71
72 if (!fs->Exists(fs_path)) {
73 std::string parent_dir, child_dir;
74 std::tie(parent_dir, child_dir) = FileFinder::GetPathAndFilename(fs_path);
75
76 if (!parent_dir.empty()) {
77 // Go up and determine the proper casing of the folder
78 auto* parent_tree = ListDirectory(parent_dir);
79 if (!parent_tree) {
80 return nullptr;
81 }
82
83 auto parent_key = make_key(parent_dir);
84 auto parent_it = dir_cache.find(parent_key);
85 assert(parent_it != dir_cache.end());
86
87 auto child_key = make_key(child_dir);
88 auto child_it = parent_tree->find(child_key);
89 if (child_it != parent_tree->end()) {
90 fs_path = FileFinder::MakePath(parent_it->second, child_it->second.name);
91 } else {
92 return nullptr;
93 }
94 }
95 }
96
97 if (!fs->GetDirectoryContent(fs_path, entries)) {
98 return nullptr;
99 }
100
101 dir_cache[dir_key] = fs_path;
102
103 DirectoryListType fs_cache_entry;
104
105 for (auto& entry : entries) {
106 std::string new_entry_key = make_key(entry.name);
107
108 if (entry.type == FileType::Directory) {
109 if (fs_cache_entry.find(new_entry_key) != fs_cache_entry.end()) {
110 Output::Warning("The folder \"{}\" exists twice.", entry.name);
111 Output::Warning("This can lead to file not found errors. Merge the directories manually in a file browser.");
112 }
113 }
114 fs_cache_entry.emplace(std::make_pair(std::move(new_entry_key), entry));
115 }
116 fs_cache.emplace(dir_key, fs_cache_entry);
117
118 return &fs_cache.find(dir_key)->second;
119 }
120
ClearCache(StringView path) const121 void DirectoryTree::ClearCache(StringView path) const {
122 DebugLog("ClearCache: {}", path);
123
124 if (path.empty()) {
125 fs_cache.clear();
126 dir_cache.clear();
127 return;
128 }
129
130 auto dir_key = make_key(path);
131 auto fs_it = fs_cache.find(dir_key);
132 if (fs_it != fs_cache.end()) {
133 fs_cache.erase(fs_it);
134 }
135 auto dir_it = dir_cache.find(dir_key);
136 if (dir_it != dir_cache.end()) {
137 dir_cache.erase(dir_it);
138 }
139 }
140
FindFile(StringView filename,Span<StringView> exts) const141 std::string DirectoryTree::FindFile(StringView filename, Span<StringView> exts) const {
142 return FindFile({ ToString(filename), exts });
143 }
144
FindFile(StringView directory,StringView filename,Span<StringView> exts) const145 std::string DirectoryTree::FindFile(StringView directory, StringView filename, Span<StringView> exts) const {
146 return FindFile({ FileFinder::MakePath(directory, filename), exts });
147 }
148
FindFile(const DirectoryTree::Args & args) const149 std::string DirectoryTree::FindFile(const DirectoryTree::Args& args) const {
150 std::string dir, name, canonical_path;
151 // Few games (e.g. Yume2kki) use path traversal (..) in the filenames to point
152 // to files outside of the actual directory.
153 canonical_path = FileFinder::MakeCanonical(args.path, args.canonical_initial_deepness);
154
155 if (args.translate && !Tr::GetCurrentTranslationId().empty()) {
156 // Search in the active language tree but do not translate again and swallow not found warnings
157 auto tr_fs = Tr::GetCurrentTranslationFilesystem();
158 auto translated_file = tr_fs.FindFile(
159 {args.path, args.exts, args.canonical_initial_deepness, false, false});
160 if (!translated_file.empty()) {
161 DebugLog("Translated {} as {}", args.path, translated_file);
162 return tr_fs.MakePath(translated_file);
163 }
164 }
165
166 std::tie(dir, name) = FileFinder::GetPathAndFilename(canonical_path);
167
168 DebugLog("FindFile: {} | {} | {} | {}", args.path, canonical_path, dir, name);
169
170 auto* entries = ListDirectory(dir);
171 if (!entries) {
172 if (args.file_not_found_warning) {
173 Output::Debug("Cannot find: {}/{}", dir, name);
174 }
175 return "";
176 }
177
178 std::string dir_key = make_key(dir);
179 auto dir_it = dir_cache.find(dir_key);
180 assert(dir_it != dir_cache.end());
181
182 std::string name_key = make_key(name);
183 if (args.exts.empty()) {
184 auto entry_it = entries->find(name_key);
185 if (entry_it != entries->end() && entry_it->second.type == FileType::Regular) {
186 return FileFinder::MakePath(dir_it->second, entry_it->second.name);
187 }
188 } else {
189 for (const auto& ext : args.exts) {
190 auto full_name_key = name_key + ToString(ext);
191 auto entry_it = entries->find(full_name_key);
192 if (entry_it != entries->end() && entry_it->second.type == FileType::Regular) {
193 return FileFinder::MakePath(dir_it->second, entry_it->second.name);
194 }
195 }
196 }
197
198 if (args.file_not_found_warning) {
199 Output::Debug("Cannot find: {}/{}", dir, name);
200 }
201
202 return "";
203 }
204