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