1 // Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
2 // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
3 
4 #include "FileSystem.h"
5 #include "StringRange.h"
6 #include "libs.h"
7 #include <algorithm>
8 #include <cassert>
9 #include <iterator>
10 #include <sstream>
11 #include <stdexcept>
12 
13 namespace FileSystem {
14 
15 	static FileSourceFS dataFilesApp(GetDataDir(), true);
16 	static FileSourceFS dataFilesUser(JoinPath(GetUserDir(), "data"));
17 	FileSourceUnion gameDataFiles;
18 	FileSourceFS userFiles(GetUserDir());
19 
20 	// note: some functions (GetUserDir(), GetDataDir()) are in FileSystem{Posix,Win32}.cpp
SanitiseFileName(const std::string & a)21 	std::string SanitiseFileName(const std::string &a)
22 	{
23 		const char *disabled_chars = "\\/'\":?<>|&*";
24 		std::ostringstream ss;
25 		if (a.empty() || (a[0] == '.')) {
26 			ss << "x";
27 		}
28 
29 		for (const char *c = a.c_str(), *end = c + a.size(); c != end; ++c) {
30 			if (strchr(disabled_chars, *c) || (*c < ' ')) {
31 				ss << "_";
32 				ss.setf(std::ios::hex, std::ios::basefield);
33 				ss.fill('0');
34 				ss.width(2);
35 				ss << int(*c);
36 			} else if (*c == ' ') {
37 				ss << '_';
38 			} else {
39 				ss << *c;
40 			}
41 		}
42 
43 		return ss.str();
44 	}
45 
JoinPath(const std::string & a,const std::string & b)46 	std::string JoinPath(const std::string &a, const std::string &b)
47 	{
48 		if (!b.empty()) {
49 			if (b[0] == '/' || a.empty())
50 				return b;
51 			else
52 				return a + "/" + b;
53 		} else
54 			return a;
55 	}
56 
normalise_path(std::string & result,const StringRange & path)57 	static void normalise_path(std::string &result, const StringRange &path)
58 	{
59 		StringRange part(path.begin, path.end);
60 		if (!path.Empty() && (path[0] == '/')) {
61 			result += '/';
62 			++part.begin;
63 		}
64 		const size_t initial_result_length = result.size();
65 		while (true) {
66 			part.end = part.FindChar('/'); // returns part.end if the char is not found
67 			if (part.Empty() || (part == ".")) {
68 				// skip this part
69 			} else if (part == "..") {
70 				// pop the last component
71 				if (result.size() <= initial_result_length)
72 					throw std::invalid_argument(path.ToString());
73 				size_t pos = result.rfind('/');
74 				if (pos == std::string::npos) {
75 					pos = 0;
76 				}
77 				assert(pos >= initial_result_length);
78 				result.erase(pos);
79 			} else {
80 				// push the new component
81 				if (result.size() > initial_result_length)
82 					result += '/';
83 				result.append(part.begin, part.Size());
84 			}
85 			if (part.end == path.end) {
86 				break;
87 			}
88 			assert(*part.end == '/');
89 			part.begin = part.end + 1;
90 			part.end = path.end;
91 		}
92 	}
93 
NormalisePath(const std::string & path)94 	std::string NormalisePath(const std::string &path)
95 	{
96 		std::string result;
97 		result.reserve(path.size());
98 		normalise_path(result, StringRange(path.c_str(), path.size()));
99 		return result;
100 	}
101 
JoinPathBelow(const std::string & base,const std::string & path)102 	std::string JoinPathBelow(const std::string &base, const std::string &path)
103 	{
104 		if (base.empty())
105 			return path;
106 		if (!path.empty()) {
107 			if ((path[0] == '/') && (base != "/"))
108 				throw std::invalid_argument(path);
109 			else {
110 				std::string result(base);
111 				result.reserve(result.size() + 1 + path.size());
112 				if (result[result.size() - 1] != '/')
113 					result += '/';
114 				StringRange rhs(path.c_str(), path.size());
115 				if (path[0] == '/') {
116 					assert(base == "/");
117 					++rhs.begin;
118 				}
119 				normalise_path(result, rhs);
120 				return result;
121 			}
122 		} else
123 			return base;
124 	}
125 
GetRelativePath(const std::string & base,const std::string & path)126 	std::string GetRelativePath(const std::string &base, const std::string &path)
127 	{
128 		// catch all common errors early
129 		if (base.empty() || path.empty() || path.size() < base.size())
130 			return path;
131 
132 		// can't strip a non-root prefix from a root path and vice-versa
133 		if ((path[0] == '/' || base[0] == '/') && (path[0] != base[0]))
134 			return path;
135 
136 		// check if <path> exactly starts with <base>
137 		if (path.compare(0, base.size(), base) == 0) {
138 			if (path.size() == base.size())
139 				return ""; // strip the entire base and return an empty path
140 			else if (path[base.size()] == '/')
141 				return path.substr(base.size() + 1); // strip the base and the trailing separator
142 		}
143 
144 		// if <path> isn't relative to <base>, return the original <path>
145 		return path;
146 	}
147 
CopyDir(FileSource & sourceFS,std::string sourceDir,FileSourceFS & targetFS,std::string targetDir,FileSystem::CopyMode copymode)148 	bool CopyDir(FileSource &sourceFS, std::string sourceDir, FileSourceFS &targetFS, std::string targetDir, FileSystem::CopyMode copymode)
149 	{
150 		// NOTE: copymode var is not used, because only mode ONLY_MISSING_IN_TARGET is implemented
151 		if (!sourceFS.Lookup(sourceDir).IsDir() || !targetFS.Lookup(targetDir).IsDir())
152 			return false;
153 
154 		// collect files, that are already in the target
155 		// NOTE: modification time (in map value) probably will be needed in another mode
156 		std::map<std::string, Time::DateTime> targetFiles;
157 		if (copymode != CopyMode::OVERWRITE) {
158 			// don't bother collecting target file data if we're overwriting
159 			for (FileSystem::FileEnumerator files(targetFS, targetDir, FileSystem::FileEnumerator::Recurse | FileSystem::FileEnumerator::IncludeDirs); !files.Finished(); files.Next())
160 				targetFiles[files.Current().GetPath()] = files.Current().GetModificationTime();
161 		}
162 
163 		for (FileSystem::FileEnumerator files(sourceFS, sourceDir, FileSystem::FileEnumerator::Recurse | FileSystem::FileEnumerator::IncludeDirs); !files.Finished(); files.Next()) {
164 			const FileSystem::FileInfo &info = files.Current();
165 			const std::string targetPath = FileSystem::JoinPathBelow(targetDir, FileSystem::GetRelativePath(sourceDir, info.GetPath()));
166 			const auto &oldFile = targetFiles.find(targetPath);
167 			if (oldFile == targetFiles.end()) { // there is no such file (or dir) in the target
168 				if (info.IsFile()) {
169 					//copy file
170 					RefCountedPtr<FileData> fileData = info.Read();
171 					FILE *outfile = targetFS.OpenWriteStream(targetPath);
172 					fwrite(fileData->GetData(), 1, fileData->GetSize(), outfile);
173 					fclose(outfile);
174 					// Output("copy %s to %s\n", info.GetAbsolutePath(), FileSystem::JoinPath(targetFS.GetRoot(), targetPath));
175 				} else if (info.IsDir()) {
176 					//create the subdir
177 					targetFS.MakeDirectory(targetPath);
178 					// Output("create dir %s\n", FileSystem::JoinPath(targetFS.GetRoot(), targetPath));
179 				}
180 			} else
181 				//this file is no longer needed when searching
182 				targetFiles.erase(oldFile);
183 		}
184 
185 		return true;
186 	}
187 
Init()188 	void Init()
189 	{
190 		gameDataFiles.AppendSource(&dataFilesUser);
191 		gameDataFiles.AppendSource(&dataFilesApp);
192 	}
193 
Uninit()194 	void Uninit()
195 	{
196 	}
197 
FileInfo(FileSource * source,const std::string & path,FileType type,Time::DateTime modTime)198 	FileInfo::FileInfo(FileSource *source, const std::string &path, FileType type, Time::DateTime modTime) :
199 		m_source(source),
200 		m_path(path),
201 		m_modTime(modTime),
202 		m_dirLen(0),
203 		m_type(type)
204 	{
205 		assert((m_path.size() <= 1) || (m_path[m_path.size() - 1] != '/'));
206 		std::size_t slashpos = m_path.rfind('/');
207 		if (slashpos != std::string::npos) {
208 			m_dirLen = slashpos + 1;
209 		} else {
210 			m_dirLen = 0;
211 		}
212 	}
213 
MakeFileInfo(const std::string & path,FileInfo::FileType fileType,Time::DateTime modTime)214 	FileInfo FileSource::MakeFileInfo(const std::string &path, FileInfo::FileType fileType, Time::DateTime modTime)
215 	{
216 		return FileInfo(this, path, fileType, modTime);
217 	}
218 
MakeFileInfo(const std::string & path,FileInfo::FileType fileType)219 	FileInfo FileSource::MakeFileInfo(const std::string &path, FileInfo::FileType fileType)
220 	{
221 		return MakeFileInfo(path, fileType, Time::DateTime());
222 	}
223 
FileSourceUnion()224 	FileSourceUnion::FileSourceUnion() :
225 		FileSource(":union:") {}
~FileSourceUnion()226 	FileSourceUnion::~FileSourceUnion() {}
227 
PrependSource(FileSource * fs)228 	void FileSourceUnion::PrependSource(FileSource *fs)
229 	{
230 		assert(fs);
231 		RemoveSource(fs);
232 		m_sources.insert(m_sources.begin(), fs);
233 	}
234 
AppendSource(FileSource * fs)235 	void FileSourceUnion::AppendSource(FileSource *fs)
236 	{
237 		assert(fs);
238 		RemoveSource(fs);
239 		m_sources.push_back(fs);
240 	}
241 
RemoveSource(FileSource * fs)242 	void FileSourceUnion::RemoveSource(FileSource *fs)
243 	{
244 		std::vector<FileSource *>::iterator nend = std::remove(m_sources.begin(), m_sources.end(), fs);
245 		m_sources.erase(nend, m_sources.end());
246 	}
247 
Lookup(const std::string & path)248 	FileInfo FileSourceUnion::Lookup(const std::string &path)
249 	{
250 		for (std::vector<FileSource *>::const_iterator
251 				 it = m_sources.begin();
252 			 it != m_sources.end(); ++it) {
253 			FileInfo info = (*it)->Lookup(path);
254 			if (info.Exists()) {
255 				return info;
256 			}
257 		}
258 		return MakeFileInfo(path, FileInfo::FT_NON_EXISTENT);
259 	}
260 
LookupAll(const std::string & path)261 	std::vector<FileInfo> FileSourceUnion::LookupAll(const std::string &path)
262 	{
263 		std::vector<FileInfo> outFiles;
264 
265 		for (FileSource *fs : m_sources) {
266 			FileInfo info = fs->Lookup(path);
267 			if (info.Exists()) outFiles.push_back(info);
268 		}
269 
270 		return outFiles;
271 	}
272 
ReadFile(const std::string & path)273 	RefCountedPtr<FileData> FileSourceUnion::ReadFile(const std::string &path)
274 	{
275 		for (std::vector<FileSource *>::const_iterator
276 				 it = m_sources.begin();
277 			 it != m_sources.end(); ++it) {
278 			RefCountedPtr<FileData> data = (*it)->ReadFile(path);
279 			if (data) {
280 				return data;
281 			}
282 		}
283 		return RefCountedPtr<FileData>();
284 	}
285 
286 	// Merge two sets of FileInfo's, by path.
287 	// Input vectors must be sorted. Output will be sorted.
288 	// Where a path is present in both inputs, directories are selected
289 	// in preference to non-directories; otherwise, the FileInfo from the
290 	// first vector is selected in preference to the second vector.
file_union_merge(std::vector<FileInfo>::const_iterator a,std::vector<FileInfo>::const_iterator aend,std::vector<FileInfo>::const_iterator b,std::vector<FileInfo>::const_iterator bend,std::vector<FileInfo> & output)291 	static void file_union_merge(
292 		std::vector<FileInfo>::const_iterator a, std::vector<FileInfo>::const_iterator aend,
293 		std::vector<FileInfo>::const_iterator b, std::vector<FileInfo>::const_iterator bend,
294 		std::vector<FileInfo> &output)
295 	{
296 		while ((a != aend) && (b != bend)) {
297 			int order = a->GetPath().compare(b->GetPath());
298 			int which = order;
299 			if (which == 0) {
300 				if (b->IsDir() && !a->IsDir()) {
301 					which = 1;
302 				} else {
303 					which = -1;
304 				}
305 			}
306 			if (which < 0) {
307 				output.push_back(*a++);
308 				if (order == 0) ++b;
309 			} else {
310 				output.push_back(*b++);
311 				if (order == 0) ++a;
312 			}
313 		}
314 
315 		if (a != aend) {
316 			std::copy(a, aend, std::back_inserter(output));
317 		}
318 		if (b != bend) {
319 			std::copy(b, bend, std::back_inserter(output));
320 		}
321 	}
322 
ReadDirectory(const std::string & path,std::vector<FileInfo> & output)323 	bool FileSourceUnion::ReadDirectory(const std::string &path, std::vector<FileInfo> &output)
324 	{
325 		if (m_sources.empty()) {
326 			return false;
327 		}
328 		if (m_sources.size() == 1) {
329 			return m_sources.front()->ReadDirectory(path, output);
330 		}
331 
332 		bool founddir = false;
333 
334 		std::vector<FileInfo> merged;
335 		for (std::vector<FileSource *>::const_iterator
336 				 it = m_sources.begin();
337 			 it != m_sources.end(); ++it) {
338 			std::vector<FileInfo> nextfiles;
339 			if ((*it)->ReadDirectory(path, nextfiles)) {
340 				founddir = true;
341 
342 				std::vector<FileInfo> prevfiles;
343 				prevfiles.swap(merged);
344 				// merge order is important
345 				// file_union_merge selects from its first input preferentially
346 				file_union_merge(
347 					prevfiles.begin(), prevfiles.end(),
348 					nextfiles.begin(), nextfiles.end(),
349 					merged);
350 			}
351 		}
352 
353 		output.reserve(output.size() + merged.size());
354 		std::copy(merged.begin(), merged.end(), std::back_inserter(output));
355 
356 		return founddir;
357 	}
358 
FileEnumerator(FileSource & fs,int flags)359 	FileEnumerator::FileEnumerator(FileSource &fs, int flags) :
360 		m_source(&fs),
361 		m_flags(flags) {}
362 
FileEnumerator(FileSource & fs,const std::string & path,int flags)363 	FileEnumerator::FileEnumerator(FileSource &fs, const std::string &path, int flags) :
364 		m_source(&fs),
365 		m_flags(flags)
366 	{
367 		AddSearchRoot(path);
368 	}
369 
~FileEnumerator()370 	FileEnumerator::~FileEnumerator() {}
371 
AddSearchRoot(const std::string & path)372 	void FileEnumerator::AddSearchRoot(const std::string &path)
373 	{
374 		const FileInfo fi = m_source->Lookup(path);
375 		if (fi.IsDir()) {
376 			QueueDirectoryContents(fi);
377 			ExpandDirQueue();
378 		}
379 	}
380 
Next()381 	void FileEnumerator::Next()
382 	{
383 		m_queue.pop_front();
384 		ExpandDirQueue();
385 	}
386 
ExpandDirQueue()387 	void FileEnumerator::ExpandDirQueue()
388 	{
389 		while (m_queue.empty() && !m_dirQueue.empty()) {
390 			const FileInfo &nextDir = m_dirQueue.front();
391 			assert(nextDir.IsDir());
392 			QueueDirectoryContents(nextDir);
393 			m_dirQueue.pop_front();
394 		}
395 	}
396 
QueueDirectoryContents(const FileInfo & info)397 	void FileEnumerator::QueueDirectoryContents(const FileInfo &info)
398 	{
399 		assert(info.IsDir());
400 
401 		std::vector<FileInfo> entries;
402 		m_source->ReadDirectory(info.GetPath(), entries);
403 		for (std::vector<FileInfo>::const_iterator
404 				 it = entries.begin();
405 			 it != entries.end(); ++it) {
406 
407 			switch (it->GetType()) {
408 			case FileInfo::FT_DIR:
409 				if (m_flags & IncludeDirs) {
410 					m_queue.push_back(*it);
411 				}
412 				if (m_flags & Recurse) {
413 					m_dirQueue.push_back(*it);
414 				}
415 				break;
416 			case FileInfo::FT_FILE:
417 				if (!(m_flags & ExcludeFiles)) {
418 					m_queue.push_back(*it);
419 				}
420 				break;
421 			case FileInfo::FT_SPECIAL:
422 				if (m_flags & IncludeSpecials) {
423 					m_queue.push_back(*it);
424 				}
425 				break;
426 			default: assert(0); break;
427 			}
428 		}
429 	}
430 
431 } // namespace FileSystem
432