1 /**
2  * @file
3  * @brief All of UFO's data access is through a hierarchical file system, but the
4  * contents of the file system can be transparently merged from several sources.
5  * The "base directory" is the path to the directory holding the ufo binary and the game directory (base).
6  * The base directory is only used during filesystem initialization.
7  * The "game directory" is the first tree on the search path and directory that all generated
8  * files (savegames, screenshots, config files) will be saved to.
9  */
10 
11 /*
12 Copyright (C) 1997-2001 Id Software, Inc.
13 
14 This program is free software; you can redistribute it and/or
15 modify it under the terms of the GNU General Public License
16 as published by the Free Software Foundation; either version 2
17 of the License, or (at your option) any later version.
18 
19 This program is distributed in the hope that it will be useful,
20 but WITHOUT ANY WARRANTY; without even the implied warranty of
21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
22 
23 See the GNU General Public License for more details.
24 
25 You should have received a copy of the GNU General Public License
26 along with this program; if not, write to the Free Software
27 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
28 
29 */
30 
31 #include "common.h"
32 #include "qfiles.h"
33 #include "unzip.h"
34 #include "../ports/system.h"
35 #include "../shared/defines.h"
36 #include "../shared/parse.h"
37 #include <unistd.h>
38 
39 /** counter for opened files - used to check against missing close calls */
40 static int fs_openedFiles;
41 static filelink_t* fs_links;
42 static searchpath_t* fs_searchpaths;
43 #define MODS_DIR "mods"
44 
FS_CreateOpenPipeFile(const char * filename,qFILE * f)45 void FS_CreateOpenPipeFile (const char* filename, qFILE *f)
46 {
47 	if (fs_searchpaths == nullptr) {
48 		Sys_Error("Filesystem call made without initialization");
49 	}
50 
51 	OBJZERO(*f);
52 
53 	Q_strncpyz(f->name, filename, sizeof(f->name));
54 	Sys_Mkfifo(va("%s/%s", FS_Gamedir(), filename), f);
55 
56 	if (f->f != nullptr) {
57 		Com_Printf("created pipe %s\n", filename);
58 		fs_openedFiles++;
59 	}
60 }
61 
62 /**
63  * @brief Called to find where to write a file (savegames, etc)
64  * @note We will use the searchpath that isn't a pack and has highest priority
65  */
FS_Gamedir(void)66 const char* FS_Gamedir (void)
67 {
68 	searchpath_t* search;
69 
70 	for (search = fs_searchpaths; search; search = search->next) {
71 		if (search->write)
72 			return search->filename;
73 	}
74 
75 	return nullptr;
76 }
77 
78 /**
79  * @brief Convert operating systems path separators to ufo virtual filesystem
80  * separators (/)
81  * @sa Sys_NormPath
82  */
FS_NormPath(char * path)83 void FS_NormPath (char* path)
84 {
85 	Sys_NormPath(path);
86 }
87 
88 /**
89  * @brief Returns the size of a given file or -1 if no file is opened
90  */
FS_FileLength(qFILE * f)91 int FS_FileLength (qFILE * f)
92 {
93 	if (f->f) {
94 		const int pos = ftell(f->f);
95 		int end;
96 
97 		fseek(f->f, 0, SEEK_END);
98 		end = ftell(f->f);
99 		fseek(f->f, pos, SEEK_SET);
100 
101 		return end;
102 	} else if (f->z) {
103 		unz_file_info info;
104 		if (unzGetCurrentFileInfo(f->z, &info, nullptr, 0, nullptr, 0, nullptr, 0) != UNZ_OK)
105 			Sys_Error("Couldn't get size of %s", f->name);
106 		return info.uncompressed_size;
107 	}
108 
109 	return -1;
110 }
111 
112 /**
113  * @brief Creates any directories needed to store the given filename
114  * @sa Sys_Mkdir
115  * @note Paths should already be normalized
116  * @sa FS_NormPath
117  */
FS_CreatePath(const char * path)118 void FS_CreatePath (const char* path)
119 {
120 	char pathCopy[MAX_OSPATH];
121 	char* ofs;
122 
123 	Q_strncpyz(pathCopy, path, sizeof(pathCopy));
124 
125 	for (ofs = pathCopy + 1; *ofs; ofs++) {
126 		/* create the directory */
127 		if (*ofs == '/') {
128 			*ofs = 0;
129 			Sys_Mkdir(pathCopy);
130 			*ofs = '/';
131 		}
132 	}
133 }
134 
135 /**
136  * @brief Closes a file handle
137  * @sa FS_OpenFile
138  */
FS_CloseFile(qFILE * f)139 void FS_CloseFile (qFILE * f)
140 {
141 	if (f->f) {
142 		fclose(f->f);
143 		fs_openedFiles--;
144 	} else if (f->z) {
145 		unzCloseCurrentFile(f->z);
146 		fs_openedFiles--;
147 	} else {
148 		return;
149 	}
150 	assert(fs_openedFiles >= 0);
151 
152 	f->f = nullptr;
153 	f->z = nullptr;
154 }
155 
156 /**
157  * @brief Finds and opens the file in the search path.
158  * @param[in] filename
159  * @param[out] file The file pointer
160  * @param[in] mode read, write, append as an enum
161  * @return the filesize or -1 in case of an error
162  * @note Used for streaming data out of either a pak file or a separate file.
163  */
FS_OpenFile(const char * filename,qFILE * file,filemode_t mode)164 int FS_OpenFile (const char* filename, qFILE *file, filemode_t mode)
165 {
166 	searchpath_t* search;
167 	char netpath[MAX_OSPATH];
168 	int i;
169 	filelink_t* link;
170 
171 	file->z = file->f = nullptr;
172 
173 	/* open for write or append in gamedir and return */
174 	if (mode == FILE_WRITE || mode == FILE_APPEND) {
175 		Com_sprintf(netpath, sizeof(netpath), "%s/%s", FS_Gamedir(), filename);
176 		FS_CreatePath(netpath);
177 
178 		file->f = fopen(netpath, (mode == FILE_WRITE ? "wb" : "ab"));
179 		if (file->f) {
180 			fs_openedFiles++;
181 			return 0;
182 		}
183 
184 		return -1;
185 	}
186 
187 	Q_strncpyz(file->name, filename, sizeof(file->name));
188 
189 	/* check for links first */
190 	for (link = fs_links; link; link = link->next) {
191 		if (!strncmp(filename, link->from, link->fromlength)) {
192 			int length;
193 			Com_sprintf(netpath, sizeof(netpath), "%s%s", link->to, filename + link->fromlength);
194 			length = FS_OpenFile(netpath, file, mode);
195 			Q_strncpyz(file->name, filename, sizeof(file->name));
196 			if (length == -1)
197 				Com_Printf("linked file could not be opened: %s\n", netpath);
198 			return length;
199 		}
200 	}
201 
202 	/* search through the path, one element at a time */
203 	for (search = fs_searchpaths; search; search = search->next) {
204 		/* is the element a pak file? */
205 		if (search->pack) {
206 			/* look through all the pak file elements */
207 			pack_t* pak = search->pack;
208 			for (i = 0; i < pak->numfiles; i++) {
209 				/* found it! */
210 				if (!Q_strcasecmp(pak->files[i].name, filename)) {
211 					/* open a new file on the pakfile */
212 					if (unzLocateFile(pak->handle.z, filename, 2) == UNZ_OK) {	/* found it! */
213 						if (unzOpenCurrentFile(pak->handle.z) == UNZ_OK) {
214 							unz_file_info info;
215 							if (unzGetCurrentFileInfo(pak->handle.z, &info, nullptr, 0, nullptr, 0, nullptr, 0) != UNZ_OK)
216 								Sys_Error("Couldn't get size of %s in %s", filename, pak->filename);
217 							unzGetCurrentFileInfoPosition(pak->handle.z, &file->filepos);
218 							file->z = pak->handle.z;
219 							fs_openedFiles++;
220 							return info.uncompressed_size;
221 						}
222 					}
223 					return pak->files[i].filelen;
224 				}
225 			}
226 		} else {
227 			/* check a file in the directory tree */
228 			Com_sprintf(netpath, sizeof(netpath), "%s/%s", search->filename, filename);
229 
230 			file->f = fopen(netpath, "rb");
231 			if (!file->f)
232 				continue;
233 
234 			fs_openedFiles++;
235 			return FS_FileLength(file);
236 		}
237 	}
238 
239 	file->f = nullptr;
240 	file->z = nullptr;
241 	return -1;
242 }
243 
244 #define PK3_SEEK_BUFFER_SIZE 65536
245 /**
246  * @brief Sets the file position of the given file
247  * @param[in] f The opened file handle
248  * @param[in] origin fsOrigin_t
249  * @param[in] offset The offset you want to do the
250  * @sa FS_Read
251  */
FS_Seek(qFILE * f,long offset,int origin)252 int FS_Seek (qFILE * f, long offset, int origin)
253 {
254 	if (f->z) {
255 		byte buffer[PK3_SEEK_BUFFER_SIZE];
256 		int remainder = offset;
257 
258 		if (offset < 0 || origin == FS_SEEK_END) {
259 			Sys_Error("Negative offsets and FS_SEEK_END not implemented "
260 					"for FS_Seek on pk3 file contents\n");
261 		}
262 
263 		switch (origin) {
264 		case FS_SEEK_SET:
265 			unzSetCurrentFileInfoPosition(f->z, (unsigned long)f->filepos);
266 			unzOpenCurrentFile(f->z);
267 			/* fallthrough */
268 		case FS_SEEK_CUR:
269 			while (remainder > PK3_SEEK_BUFFER_SIZE) {
270 				FS_Read(buffer, PK3_SEEK_BUFFER_SIZE, f);
271 				remainder -= PK3_SEEK_BUFFER_SIZE;
272 			}
273 			FS_Read(buffer, remainder, f);
274 			return offset;
275 
276 		default:
277 			Sys_Error("Bad origin in FS_Seek");
278 		}
279 	} else if (f->f) {
280 		int _origin;
281 		switch (origin) {
282 		case FS_SEEK_CUR:
283 			_origin = SEEK_CUR;
284 			break;
285 		case FS_SEEK_END:
286 			_origin = SEEK_END;
287 			break;
288 		case FS_SEEK_SET:
289 			_origin = SEEK_SET;
290 			break;
291 		default:
292 			Sys_Error("Bad origin in FS_Seek");
293 		}
294 		return fseek(f->f, offset, _origin);
295 	} else
296 		Sys_Error("FS_Seek: no file opened");
297 }
298 
299 /**
300  * @brief Just returns the filelength and -1 if the file wasn't found
301  * @note Won't print any errors
302  * @sa FS_FileExists
303  */
FS_CheckFile(const char * fmt,...)304 int FS_CheckFile (const char* fmt, ...)
305 {
306 	va_list ap;
307 	char filename[MAX_QPATH];
308 
309 	va_start(ap, fmt);
310 	Q_vsnprintf(filename, sizeof(filename), fmt, ap);
311 	va_end(ap);
312 
313 	ScopedFile file;
314 	return FS_OpenFile(filename, &file, FILE_READ);
315 }
316 
317 #define	MAX_READ	0x10000		/* read in blocks of 64k */
318 /**
319  * @brief Read a file into a given buffer in memory.
320  * @param[out] buffer Pointer to memory where file contents are written to.
321  * @param[in] len The length of the supplied memory area.
322  * @param[in] f The file which is to be read into the memory area.
323  * @return The length of the file contents successfully read and written to
324  * memory.
325  * @note @c buffer is not null-terminated at the end of file reading
326  * @note This function properly handles partial reads so check that the
327  * returned length matches @c len.
328  * @note Reads in blocks of 64k.
329  * @sa FS_LoadFile
330  * @sa FS_OpenFile
331  */
FS_Read2(void * buffer,int len,qFILE * f,bool failOnEmptyRead)332 int FS_Read2 (void* buffer, int len, qFILE *f, bool failOnEmptyRead)
333 {
334 	int read;
335 	byte* buf;
336 	int tries;
337 
338 	buf = (byte*) buffer;
339 
340 	if (f->z) {
341 		read = unzReadCurrentFile(f->z, buf, len);
342 		if (read == -1)
343 			Sys_Error("FS_Read (zipfile): -1 bytes read");
344 
345 		return read;
346 	}
347 
348 	int remaining = len;
349 	tries = 0;
350 	while (remaining) {
351 		int block = remaining;
352 		if (block > MAX_READ)
353 			block = MAX_READ;
354 		read = fread(buf, 1, block, f->f);
355 
356 		/* end of file reached */
357 		if (read != block && feof(f->f))
358 			return (len - remaining + read);
359 
360 		if (read == 0) {
361 			/* we might have been trying to read from a CD */
362 			if (!tries)
363 				tries = 1;
364 			else if (failOnEmptyRead)
365 				Sys_Error("FS_Read: 0 bytes read");
366 			else
367 				return len - remaining;
368 		}
369 
370 		if (read == -1)
371 			Sys_Error("FS_Read: -1 bytes read");
372 
373 		/* do some progress bar thing here... */
374 		remaining -= read;
375 		buf += read;
376 	}
377 	return len;
378 }
379 
FS_Read(void * buffer,int len,qFILE * f)380 int FS_Read (void* buffer, int len, qFILE * f)
381 {
382 	return FS_Read2(buffer, len, f, true);
383 }
384 
385 /**
386  * @brief Filenames are relative to the quake search path
387  * @param[in] buffer a null buffer will just return the file length without loading
388  * @param[in] path
389  * @return a -1 length means that the file is not present
390  * @sa FS_Read
391  * @sa FS_OpenFile
392  */
FS_LoadFile(const char * path,byte ** buffer)393 int FS_LoadFile (const char* path, byte** buffer)
394 {
395 	ScopedFile h;
396 	int len;
397 
398 	/* look for it in the filesystem or pack files */
399 	len = FS_OpenFile(path, &h, FILE_READ);
400 	if (!h) {
401 		if (buffer)
402 			*buffer = nullptr;
403 		return -1;
404 	}
405 
406 	if (!buffer) {
407 		return len;
408 	}
409 
410 	byte* const buf = Mem_PoolAllocTypeN(byte, len + 1, com_fileSysPool);
411 	if (!buf)
412 		return -1;
413 	*buffer = buf;
414 
415 	FS_Read(buf, len, &h);
416 	buf[len] = 0;
417 
418 	return len;
419 }
420 
FS_FreeFile(void * buffer)421 void FS_FreeFile (void* buffer)
422 {
423 	_Mem_Free(buffer, "FS_FreeFile", 0);
424 }
425 
426 /**
427  * @brief Takes an explicit (not game tree related) path to a pak file.
428  * Adding the files at the beginning of the list so they override previous pack files.
429  * @param[in] packfile The pack filename
430  * @note pk3 and zip are valid extensions
431  */
FS_LoadPackFile(const char * packfile)432 static pack_t* FS_LoadPackFile (const char* packfile)
433 {
434 	const char* extension = Com_GetExtension(packfile);
435 
436 	if (Q_streq(extension, "pk3") || Q_streq(extension, "zip")) {
437 		int i;
438 		unz_file_info file_info;
439 		unz_global_info gi;
440 		unzFile uf = unzOpen(packfile);
441 		unsigned int err = unzGetGlobalInfo(uf, &gi);
442 		char filenameInZip[MAX_QPATH];
443 
444 		if (err != UNZ_OK) {
445 			Com_Printf("Could not load '%s'\n", packfile);
446 			return nullptr;
447 		}
448 
449 		unzGoToFirstFile(uf);
450 		for (i = 0; i < gi.number_entry; i++) {
451 			err = unzGetCurrentFileInfo(uf, &file_info, filenameInZip, sizeof(filenameInZip), nullptr, 0, nullptr, 0);
452 			if (err != UNZ_OK) {
453 				break;
454 			}
455 			unzGoToNextFile(uf);
456 		}
457 
458 		pack_t* const pack = Mem_PoolAllocType(pack_t, com_fileSysPool);
459 		Q_strncpyz(pack->filename, packfile, sizeof(pack->filename));
460 		pack->handle.z = uf;
461 		pack->handle.f = nullptr;
462 		pack->numfiles = gi.number_entry;
463 		unzGoToFirstFile(uf);
464 
465 		/* Allocate space for array of packfile structures (filename, offset, length) */
466 		packfile_t* const newfiles = Mem_PoolAllocTypeN(packfile_t, i, com_fileSysPool);
467 
468 		for (i = 0; i < gi.number_entry; i++) {
469 			err = unzGetCurrentFileInfo(uf, &file_info, filenameInZip, sizeof(filenameInZip), nullptr, 0, nullptr, 0);
470 			if (err != UNZ_OK)
471 				break;
472 			Q_strlwr(filenameInZip);
473 
474 			unzGetCurrentFileInfoPosition(uf, &newfiles[i].filepos);
475 			Q_strncpyz(newfiles[i].name, filenameInZip, sizeof(newfiles[i].name));
476 			newfiles[i].filelen = file_info.compressed_size;
477 			unzGoToNextFile(uf);
478 		}
479 		pack->files = newfiles;
480 
481 		/* Sort our list alphabetically - also rearrange the unsigned long values */
482 		qsort((void*)pack->files, i, sizeof(*newfiles), Q_StringSort);
483 
484 		Com_Printf("Added packfile %s (%li files)\n", packfile, gi.number_entry);
485 		return pack;
486 	} else {
487 		/* Unrecognized file type! */
488 		Com_Printf("Pack file type %s unrecognized\n", extension);
489 		return nullptr;
490 	}
491 }
492 
493 #define MAX_PACKFILES 1024
494 
495 static char const* const pakFileExt[] = {
496 	"pk3", "zip", nullptr
497 };
498 
499 /**
500  * @brief Adds the directory to the head of the search path
501  * @note No ending slash here
502  * @param[in] dir The directory name relative to the game dir
503  * @param[in] write Add this directory as writable (config files, save games)
504  */
FS_AddGameDirectory(const char * dir,bool write)505 void FS_AddGameDirectory (const char* dir, bool write)
506 {
507 	int ndirs = 0, i;
508 	char pakfile_list[MAX_PACKFILES][MAX_OSPATH];
509 	int pakfile_count = 0;
510 	char pattern[MAX_OSPATH];
511 
512 	for (searchpath_t* search = fs_searchpaths; search; search = search->next) {
513 		if (Q_streq(search->filename, dir))
514 			return;
515 		if (write && search->write) {
516 			Com_Printf("change writing directory to %s\n", dir);
517 			search->write = false;
518 		}
519 	}
520 
521 	Com_Printf("Adding game dir: %s\n", dir);
522 
523 	for (char const* const* extList = pakFileExt; *extList; ++extList) {
524 		Com_sprintf(pattern, sizeof(pattern), "%s/*.%s", dir, *extList);
525 		char** dirnames = nullptr;
526 		dirnames = FS_ListFiles(pattern, &ndirs, 0, SFF_SUBDIR | SFF_HIDDEN | SFF_SYSTEM);
527 		if (dirnames != nullptr) {
528 			for (i = 0; i < ndirs - 1; i++) {
529 				if (strrchr(dirnames[i], '/')) {
530 					Q_strncpyz(pakfile_list[pakfile_count], dirnames[i], sizeof(pakfile_list[pakfile_count]));
531 					pakfile_count++;
532 					if (pakfile_count >= MAX_PACKFILES) {
533 						Com_Printf("Warning: Max allowed pakfiles reached (%i) - skipping the rest\n", MAX_PACKFILES);
534 						break;
535 					}
536 				}
537 				Mem_Free(dirnames[i]);
538 			}
539 			Mem_Free(dirnames);
540 		}
541 	}
542 
543 	/* Sort our list alphabetically */
544 	qsort((void*)pakfile_list, pakfile_count, MAX_OSPATH, Q_StringSort);
545 
546 	for (i = 0; i < pakfile_count; i++) {
547 		pack_t* pak = FS_LoadPackFile(pakfile_list[i]);
548 		if (!pak)
549 			continue;
550 
551 		searchpath_t* const search = Mem_PoolAllocType(searchpath_t, com_fileSysPool);
552 		search->pack = pak;
553 		search->next = fs_searchpaths;
554 		search->write = false;
555 		fs_searchpaths = search;
556 	}
557 
558 	/* add the directory to the search path */
559 	searchpath_t* const search = Mem_PoolAllocType(searchpath_t, com_fileSysPool);
560 	Q_strncpyz(search->filename, dir, sizeof(search->filename));
561 	search->next = fs_searchpaths;
562 	search->write = write;
563 	fs_searchpaths = search;
564 }
565 
566 /**
567  * @brief Builds a qsorted filelist
568  * @sa Sys_FindFirst
569  * @sa Sys_FindNext
570  * @sa Sys_FindClose
571  * @note Don't forget to free the filelist array and the file itself
572  */
FS_ListFiles(const char * findname,int * numfiles,unsigned musthave,unsigned canthave)573 char** FS_ListFiles (const char* findname, int* numfiles, unsigned musthave, unsigned canthave)
574 {
575 	char* s;
576 	int nfiles = 0, i;
577 	char tempList[MAX_FILES][MAX_OSPATH];
578 
579 	*numfiles = 0;
580 
581 	s = Sys_FindFirst(findname, musthave, canthave);
582 	while (s) {
583 		if (s[strlen(s) - 1] != '.')
584 			nfiles++;
585 		s = Sys_FindNext(musthave, canthave);
586 	}
587 	Sys_FindClose();
588 
589 	if (!nfiles)
590 		return nullptr;
591 
592 	nfiles++; /* add space for a guard */
593 	*numfiles = nfiles;
594 
595 	char** const list = Mem_PoolAllocTypeN(char*, nfiles, com_fileSysPool);
596 	OBJZERO(tempList);
597 
598 	s = Sys_FindFirst(findname, musthave, canthave);
599 	nfiles = 0;
600 	while (s) {
601 		if (s[strlen(s) - 1] != '.') {
602 			Q_strncpyz(tempList[nfiles], s, sizeof(tempList[nfiles]));
603 #ifdef _WIN32
604 			Q_strlwr(tempList[nfiles]);
605 #endif
606 			nfiles++;
607 			if (nfiles >= MAX_FILES)
608 				break;
609 		}
610 		s = Sys_FindNext(musthave, canthave);
611 	}
612 	Sys_FindClose();
613 
614 	qsort(tempList, nfiles, MAX_OSPATH, Q_StringSort);
615 	for (i = 0; i < nfiles; i++) {
616 		list[i] = Mem_PoolStrDup(tempList[i], com_fileSysPool, 0);
617 	}
618 
619 	return list;
620 }
621 
622 /**
623  * @brief Allows enumerating all of the directories in the search path
624  * @note ignore pk3 here
625  */
FS_NextPath(const char * prevpath)626 const char* FS_NextPath (const char* prevpath)
627 {
628 	searchpath_t* s;
629 	char* prev;
630 
631 	if (!prevpath)
632 		return FS_Gamedir();
633 
634 	prev = nullptr;
635 	for (s = fs_searchpaths; s; s = s->next) {
636 		if (s->pack)
637 			continue;
638 		if (prev && Q_streq(prevpath, prev))
639 			return s->filename;
640 		prev = s->filename;
641 	}
642 
643 	return nullptr;
644 }
645 
FS_GetHomeDirectory(char * gdir,size_t length)646 static bool FS_GetHomeDirectory (char* gdir, size_t length)
647 {
648 	const char* homedir = Sys_GetHomeDirectory();
649 
650 	if (homedir) {
651 #ifdef _WIN32
652 		Com_sprintf(gdir, length, "%s/" UFO_VERSION, homedir);
653 #elif defined (__APPLE__) || defined (MACOSX)
654 		Com_sprintf(gdir, length, "%s/Documents/UFOAI-" UFO_VERSION, homedir);
655 #else
656 		Com_sprintf(gdir, length, "%s/.ufoai/" UFO_VERSION, homedir);
657 #endif
658 		return true;
659 	}
660 	Com_Printf("could not find the home directory\n");
661 	return false;
662 }
663 
664 /**
665  * @note e.g. *nix: Use ~/.ufoai/dir as gamedir
666  * @param[in] dir The directory name relative to the game dir
667  * @param[in] write Add this directory as writable (config files, save games)
668  * @sa Sys_GetHomeDirectory
669  */
FS_AddHomeAsGameDirectory(const char * dir,bool write)670 void FS_AddHomeAsGameDirectory (const char* dir, bool write)
671 {
672 	char gdir[MAX_OSPATH];
673 
674 	if (FS_GetHomeDirectory(gdir, sizeof(gdir))) {
675 		Q_strcat(gdir, sizeof(gdir), "/%s", dir);
676 		FS_CreatePath(va("%s/", gdir));
677 		FS_AddGameDirectory(gdir, write);
678 	}
679 }
680 
681 /**
682  * @brief Adds the directory to the head of the search path
683  * @note No ending slash here
684  */
FS_GetModList(linkedList_t ** mods)685 int FS_GetModList (linkedList_t** mods)
686 {
687 	char gdir[MAX_OSPATH];
688 	const char* homedir;
689 
690 	if (FS_GetHomeDirectory(gdir, sizeof(gdir))) {
691 		char const * const append = "/" MODS_DIR;
692 		Q_strcat(gdir, sizeof(gdir), append);
693 		homedir = gdir;
694 	} else {
695 		homedir = nullptr;
696 	}
697 
698 	char const * searchpaths[] = {
699 #ifdef PKGDATADIR
700 			PKGDATADIR "/" MODS_DIR,
701 #endif
702 			"./" MODS_DIR,
703 			homedir,
704 			nullptr
705 	};
706 
707 	LIST_AddString(mods, BASEDIRNAME);
708 	int numberMods = 1;
709 	/* it is likely that we have duplicate names now, which we will cleanup below */
710 	for (const char** path = searchpaths; *path; path++) {
711 		const char* pattern = *path;
712 		int ndirs = 0;
713 		char** dirnames = FS_ListFiles(va("%s/*", pattern), &ndirs, SFF_SUBDIR, SFF_HIDDEN | SFF_SYSTEM);
714 		if (dirnames != nullptr) {
715 			int i;
716 			for (i = 0; i < ndirs - 1; i++) {
717 				LIST_AddString(mods, dirnames[i] + (strlen(pattern) + 1));
718 				numberMods++;
719 				Mem_Free(dirnames[i]);
720 			}
721 			Mem_Free(dirnames);
722 		}
723 	}
724 
725 	return numberMods;
726 }
727 
728 #ifdef COMPILE_UFO
729 
FS_Mod_f(void)730 static void FS_Mod_f (void)
731 {
732 	if (Cmd_Argc() == 1) {
733 		linkedList_t* list = nullptr;
734 		FS_GetModList(&list);
735 		LIST_Foreach(list, const char, mod) {
736 			Com_Printf("mod: %s\n", mod);
737 		}
738 		LIST_Delete(&list);
739 	} else {
740 		FS_RestartFilesystem(Cmd_Argv(1));
741 	}
742 }
743 /**
744  * @brief Adds the execution of the autoexec.cfg to the command buffer
745  */
FS_ExecAutoexec(void)746 void FS_ExecAutoexec (void)
747 {
748 	char name[MAX_QPATH];
749 	searchpath_t* s;
750 
751 	/* search through all the paths for an autoexec.cfg file */
752 	for (s = fs_searchpaths; s != nullptr; s = s->next) {
753 		snprintf(name, sizeof(name), "%s/autoexec.cfg", s->filename);
754 
755 		if (Sys_FindFirst(name, 0, SFF_SUBDIR | SFF_HIDDEN | SFF_SYSTEM)) {
756 			Cbuf_AddText("exec autoexec.cfg\n");
757 			Sys_FindClose();
758 			break;
759 		}
760 
761 		Sys_FindClose();
762 	}
763 
764 	Cbuf_Execute();  /* execute it */
765 }
766 
767 /**
768  * @brief Creates a filelink_t
769  */
FS_Link_f(void)770 static void FS_Link_f (void)
771 {
772 	filelink_t** prev;
773 
774 	if (Cmd_Argc() != 3) {
775 		Com_Printf("Usage: %s <from> <to>\n", Cmd_Argv(0));
776 		return;
777 	}
778 
779 	/* see if the link already exists */
780 	prev = &fs_links;
781 	for (filelink_t* l = fs_links; l; l = l->next) {
782 		if (Q_streq(l->from, Cmd_Argv(1))) {
783 			Mem_Free(l->to);
784 			if (!strlen(Cmd_Argv(2))) {	/* delete it */
785 				*prev = l->next;
786 				Mem_Free(l->from);
787 				Mem_Free(l);
788 				return;
789 			}
790 			l->to = Mem_PoolStrDup(Cmd_Argv(2), com_fileSysPool, 0);
791 			return;
792 		}
793 		prev = &l->next;
794 	}
795 
796 	/* create a new link */
797 	filelink_t* const l = Mem_PoolAllocType(filelink_t, com_fileSysPool);
798 	l->next = fs_links;
799 	fs_links = l;
800 	l->from = Mem_PoolStrDup(Cmd_Argv(1), com_fileSysPool, 0);
801 	l->fromlength = strlen(l->from);
802 	l->to = Mem_PoolStrDup(Cmd_Argv(2), com_fileSysPool, 0);
803 }
804 
805 /**
806  * @brief Show the filesystem contents - also supports wildcarding
807  * @sa FS_NextPath
808  * @note No pk3 support
809  */
FS_Dir_f(void)810 static void FS_Dir_f (void)
811 {
812 	char const *wildcard = Cmd_Argc() != 1 ? Cmd_Argv(1) : "*.*";
813 	const char* path = nullptr;
814 	char findname[1024];
815 	int ndirs;
816 
817 	while ((path = FS_NextPath(path)) != nullptr) {
818 		Com_sprintf(findname, sizeof(findname), "%s/%s", path, wildcard);
819 		FS_NormPath(findname);
820 
821 		Com_Printf("Directory of %s\n", findname);
822 		Com_Printf("----\n");
823 
824 		char** dirnames = FS_ListFiles(findname, &ndirs, 0, SFF_SUBDIR | SFF_HIDDEN | SFF_SYSTEM);
825 		if (dirnames != nullptr) {
826 			int i;
827 
828 			for (i = 0; i < ndirs - 1; i++) {
829 				char const *const slash = strrchr(dirnames[i], '/');
830 				Com_Printf("%s\n", slash ? slash + 1 : dirnames[i]);
831 
832 				Mem_Free(dirnames[i]);
833 			}
834 			Mem_Free(dirnames);
835 		}
836 		Com_Printf("\n");
837 	}
838 }
839 
FS_List_f(void)840 static void FS_List_f (void)
841 {
842 	char const *wildcard = Cmd_Argc() == 2 ? Cmd_Argv(1) : "*.*";
843 	const char* filename;
844 
845 	Com_Printf("Show files for '%s'\n", wildcard);
846 	FS_BuildFileList(wildcard);
847 	while ((filename = FS_NextFileFromFileList(wildcard)) != nullptr)
848 		Com_Printf("%s\n", filename);
849 	FS_NextFileFromFileList(nullptr);
850 }
851 
852 /**
853  * @brief Print all searchpaths
854  */
FS_Info_f(void)855 static void FS_Info_f (void)
856 {
857 	searchpath_t* search;
858 	filelink_t* l;
859 
860 	Com_Printf("Filesystem information\n");
861 	Com_Printf("...write dir: '%s'\n", FS_Gamedir());
862 
863 	for (search = fs_searchpaths; search; search = search->next) {
864 		if (search->pack == nullptr)
865 			Com_Printf("...path: '%s'\n", search->filename);
866 		else
867 			Com_Printf("...pakfile: '%s' (%i files)\n", search->pack->filename, search->pack->numfiles);
868 	}
869 
870 	for (l = fs_links; l; l = l->next)
871 		Com_Printf("...link: %s : %s\n", l->from, l->to);
872 }
873 
FS_RestartFilesystem_f(void)874 static void FS_RestartFilesystem_f (void)
875 {
876 	if (Cmd_Argc() == 2)
877 		FS_RestartFilesystem(Cmd_Argv(1));
878 	else
879 		FS_RestartFilesystem(nullptr);
880 }
881 
882 /**
883  * @brief filesystem console commands
884  * @sa FS_InitFilesystem
885  * @sa FS_RestartFilesystem
886  */
887 static const cmdList_t fs_commands[] = {
888 	{"fs_restart", FS_RestartFilesystem_f, "Reloads the file subsystem"},
889 	{"link", FS_Link_f, "Create file links"},
890 	{"dir", FS_Dir_f, "Show the filesystem contents per game dir - also supports wildcarding"},
891 	{"ls", FS_List_f, "Show the filesystem contents"},
892 	{"fs_info", FS_Info_f, "Show information about the virtual filesystem"},
893 	{"fs_mod", FS_Mod_f, "Show or activate mods"},
894 
895 	{nullptr, nullptr, nullptr}
896 };
897 
FS_RemoveCommands(void)898 static void FS_RemoveCommands (void)
899 {
900 	const cmdList_t* commands;
901 
902 	for (commands = fs_commands; commands->name; commands++)
903 		Cmd_RemoveCommand(commands->name);
904 }
905 
FS_InitCommandsAndCvars(void)906 static void FS_InitCommandsAndCvars (void)
907 {
908 	const cmdList_t* commands;
909 
910 	for (commands = fs_commands; commands->name; commands++)
911 		Cmd_AddCommand(commands->name, commands->function, commands->description);
912 }
913 #endif
914 
915 /**
916  * @sa FS_Shutdown
917  * @sa FS_RestartFilesystem
918  */
FS_InitFilesystem(bool writeToHomeDir)919 void FS_InitFilesystem (bool writeToHomeDir)
920 {
921 	Com_Printf("\n---- filesystem initialization -----\n");
922 
923 #ifdef PKGDATADIR
924 	/* add the system search path */
925 	FS_AddGameDirectory(PKGDATADIR "/" BASEDIRNAME, false);
926 #endif
927 
928 	FS_AddGameDirectory("./" BASEDIRNAME, !writeToHomeDir);
929 	FS_AddHomeAsGameDirectory(BASEDIRNAME, writeToHomeDir);
930 #ifdef COMPILE_UFO
931 	const char* fsGameDir = Cvar_GetString("fs_gamedir");
932 	if (Q_strvalid(fsGameDir)) {
933 		char path[MAX_QPATH];
934 		Com_sprintf(path, sizeof(path), "./%s", fsGameDir);
935 		if (!FS_FileExists("%s", path)) {
936 			Com_sprintf(path, sizeof(path), "./" MODS_DIR "/%s", fsGameDir);
937 		}
938 		FS_AddGameDirectory(path, !writeToHomeDir);
939 		FS_AddHomeAsGameDirectory(fsGameDir, writeToHomeDir);
940 	}
941 #endif
942 
943 #ifdef COMPILE_UFO
944 	FS_InitCommandsAndCvars();
945 	Cbuf_AddText("exec filesystem.cfg\n");
946 #endif
947 
948 	Com_Printf("using %s for writing\n", FS_Gamedir());
949 }
950 
951 /** @todo This block list code is broken in terms of filename order
952  * To see the bug reduce the FL_BLOCKSIZE to 1024 and verify the order of the
953  * filenames FS_NextScriptHeader gives you - you will see that the last files
954  * will be in reversed order
955  */
956 
957 typedef struct listBlock_s {
958 	char path[MAX_QPATH];
959 	linkedList_t* files;
960 	struct listBlock_s* next;
961 } listBlock_t;
962 
963 static listBlock_t* fs_blocklist = nullptr;
964 
965 /**
966  * @brief Add one name to the filelist
967  * @note also checks for duplicates
968  * @sa FS_BuildFileList
969  */
_AddToListBlock(linkedList_t ** fl,const char * name,bool stripPath)970 static void _AddToListBlock (linkedList_t** fl, const char* name, bool stripPath)
971 {
972 	const char* f;
973 
974 	/* strip path */
975 	if (stripPath)
976 		f = Com_SkipPath(name);
977 	else
978 		f = name;
979 
980 	if (LIST_ContainsString(*fl, f))
981 		return;
982 
983 	/* add the new file */
984 	LIST_AddStringSorted(fl, f);
985 }
986 
987 /**
988  * @brief Build a filelist
989  * @param[in] fileList e.g. *.cfg to get all config files in the gamedir/ dir
990  */
FS_BuildFileList(const char * fileList)991 int FS_BuildFileList (const char* fileList)
992 {
993 	searchpath_t* search;
994 	char files[MAX_QPATH];
995 	char findname[1024];
996 	int i;
997 
998 	/* bring it into normal form */
999 	Q_strncpyz(files, fileList, sizeof(files));
1000 	FS_NormPath(files);
1001 
1002 	/* check the blocklist for older searches
1003 	 * and do a new one after deleting them */
1004 	for (listBlock_t** anchor = &fs_blocklist; *anchor;) {
1005 		listBlock_t* const block = *anchor;
1006 		if (Q_streq(block->path, files)) {
1007 			*anchor = block->next;
1008 
1009 			LIST_Delete(&block->files);
1010 			Mem_Free(block);
1011 		} else {
1012 			anchor = &block->next;
1013 		}
1014 	}
1015 
1016 	/* allocate a new block and link it into the list */
1017 	listBlock_t* const block = Mem_PoolAllocType(listBlock_t, com_fileSysPool);
1018 	block->next = fs_blocklist;
1019 	fs_blocklist = block;
1020 
1021 	/* store the search string */
1022 	Q_strncpyz(block->path, files, sizeof(block->path));
1023 
1024 	/* search for the files */
1025 	LIST_Delete(&block->files);
1026 
1027 	/* search through the path, one element at a time */
1028 	for (search = fs_searchpaths; search; search = search->next) {
1029 		/* is the element a pak file? */
1030 		if (search->pack) {
1031 			const char* ext = strrchr(files, '.');
1032 			const pack_t* pak = search->pack;
1033 			size_t l = strlen(files);
1034 			if (!ext)
1035 				break;
1036 			Q_strncpyz(findname, files, sizeof(findname));
1037 			FS_NormPath(findname);
1038 			l -= (strlen(ext) + 1);
1039 			findname[l] = '\0';
1040 
1041 			/* look through all the pak file elements */
1042 			for (i = 0; i < pak->numfiles; i++) {
1043 				/* found it! */
1044 				const char* fileNameEntry = pak->files[i].name;
1045 				bool matchAlsoInSubDirs = (findname[0] == '*' || !strncmp(fileNameEntry, findname, l))
1046 						 && (ext[0] == '*' || strstr(fileNameEntry, ext));
1047 				if (matchAlsoInSubDirs) {
1048 					bool add = false;
1049 					if (strstr(findname, "**"))
1050 						add = true;
1051 					else {
1052 						char pathName[MAX_QPATH];
1053 						char pathNameEntry[MAX_QPATH];
1054 						Com_FilePath(findname, pathName, sizeof(pathName));
1055 						Com_FilePath(fileNameEntry, pathNameEntry, sizeof(pathNameEntry));
1056 						if (Q_streq(pathNameEntry, pathName))
1057 							add = true;
1058 					}
1059 
1060 					if (add)
1061 						_AddToListBlock(&block->files, pak->files[i].name, true);
1062 				}
1063 			}
1064 		} else if (strstr(files, "**")) {
1065 			linkedList_t* list = nullptr;
1066 			const char* wildcard = strstr(files, "**");
1067 			const size_t l = strlen(files) - strlen(wildcard);
1068 
1069 			Q_strncpyz(findname, files, sizeof(findname));
1070 			FS_NormPath(findname);
1071 			findname[l] = '\0';
1072 			if (l > 0 && findname[l - 1] == '/')
1073 				findname[l - 1] = '\0';
1074 
1075 			Sys_ListFilteredFiles(search->filename, findname, &findname[l + 1], &list);
1076 
1077 			LIST_Foreach(list, const char, name) {
1078 				_AddToListBlock(&block->files, name, false);
1079 			}
1080 
1081 			LIST_Delete(&list);
1082 		} else {
1083 			int nfiles = 0;
1084 			char** filenames;
1085 
1086 			Com_sprintf(findname, sizeof(findname), "%s/%s", search->filename, files);
1087 			FS_NormPath(findname);
1088 
1089 			filenames = FS_ListFiles(findname, &nfiles, 0, SFF_HIDDEN | SFF_SYSTEM);
1090 			if (filenames != nullptr) {
1091 				for (i = 0; i < nfiles - 1; i++) {
1092 					_AddToListBlock(&block->files, filenames[i], true);
1093 					Mem_Free(filenames[i]);
1094 				}
1095 				Mem_Free(filenames);
1096 			}
1097 		}
1098 	}
1099 
1100 	return LIST_Count(block->files);
1101 }
1102 
1103 /**
1104  * @brief Returns the next file that is found in the virtual filesystem identified by the given file pattern
1105  * @param[in] files The file pattern to search for. This can e.g. be "*.ogg" or "**.ufo" to also include
1106  * subdirectories.
1107  * @return The next found filename or @c NULL if the end of the list was reached.
1108  * @note Keep in mind that the list is cached and also the position in the list is not reset until you explicitly
1109  * want this by calling this function with a @c NULL parameter for the pattern.
1110  * @note If you have to rebuild a file list, use @c FS_BuildFileList manually. Following calls will then use the new
1111  * file list.
1112  */
FS_NextFileFromFileList(const char * files)1113 const char* FS_NextFileFromFileList (const char* files)
1114 {
1115 	static linkedList_t* listEntry = nullptr;
1116 	static listBlock_t* _block = nullptr;
1117 	listBlock_t* block;
1118 	const char* file = nullptr;
1119 
1120 	/* restart the list? */
1121 	if (files == nullptr) {
1122 		_block = nullptr;
1123 		return nullptr;
1124 	}
1125 
1126 	for (block = fs_blocklist; block; block = block->next) {
1127 		if (!strncmp(files, block->path, MAX_QPATH))
1128 			break;
1129 	}
1130 
1131 	if (!block) {
1132 		FS_BuildFileList(files);
1133 		for (block = fs_blocklist; block; block = block->next) {
1134 			if (!strncmp(files, block->path, MAX_QPATH))
1135 				break;
1136 		}
1137 		if (!block) {
1138 			/* still no filelist */
1139 			Com_Printf("FS_NextFileFromFileList: Could not create filelist for %s\n", files);
1140 			return nullptr;
1141 		}
1142 	}
1143 
1144 	/* everytime we switch between different blocks we get the
1145 	 * first file again when we switch back */
1146 	if (_block != block) {
1147 		_block = block;
1148 		listEntry = block->files;
1149 	}
1150 
1151 	if (listEntry) {
1152 		file = (const char*)listEntry->data;
1153 		listEntry = listEntry->next;
1154 	}
1155 
1156 	/* finished */
1157 	return file;
1158 }
1159 
1160 /**
1161  * @brief Returns the buffer of a file
1162  * @param[in] files If nullptr, reset the filelist
1163  * If not nullptr it may be something like *.cfg to get a list
1164  * of all config files in base/. Calling @c FS_GetFileData("*.cfg");
1165  * until it returns @c nullptr is sufficient to get one buffer after another.
1166  * @note You don't have to free the file buffer on the calling side.
1167  * This is done in this function, too
1168  */
FS_GetFileData(const char * files)1169 const char* FS_GetFileData (const char* files)
1170 {
1171 	listBlock_t* block;
1172 	static linkedList_t* fileList = nullptr;
1173 	static byte* buffer = nullptr;
1174 
1175 	/* free the old file */
1176 	if (buffer) {
1177 		FS_FreeFile(buffer);
1178 		buffer = nullptr;
1179 	}
1180 
1181 	if (!files) {
1182 		fileList = nullptr;
1183 		return nullptr;
1184 	}
1185 
1186 	for (block = fs_blocklist; block; block = block->next) {
1187 		if (Q_streq(files, block->path))
1188 			break;
1189 	}
1190 
1191 	if (!block) {
1192 		/* didn't find any valid file list */
1193 		fileList = nullptr;
1194 		FS_BuildFileList(files);
1195 		for (block = fs_blocklist; block; block = block->next) {
1196 			if (Q_streq(files, block->path))
1197 				break;
1198 		}
1199 		if (!block) {
1200 			/* still no filelist */
1201 			Com_Printf("FS_GetFileData: Could not create filelist for %s\n", files);
1202 			return nullptr;
1203 		}
1204 	}
1205 
1206 	if (!fileList)
1207 		/* start the list */
1208 		fileList = block->files;
1209 	else
1210 		/* search a new file */
1211 		fileList = fileList->next;
1212 
1213 	if (fileList) {
1214 		char filename[MAX_QPATH];
1215 
1216 		/* load a new file */
1217 		Q_strncpyz(filename, block->path, sizeof(filename));
1218 		strcpy(strrchr(filename, '/') + 1, (const char*)fileList->data);
1219 
1220 		FS_LoadFile(filename, &buffer);
1221 		return (const char*)buffer;
1222 	}
1223 
1224 	/* finished */
1225 	return nullptr;
1226 }
1227 
FS_NextScriptHeader(const char * files,const char ** name,const char ** text)1228 char* FS_NextScriptHeader (const char* files, const char** name, const char** text)
1229 {
1230 	static char lastList[MAX_QPATH];
1231 	static listBlock_t* lBlock;
1232 	static linkedList_t* lFile;
1233 	static byte* lBuffer;
1234 
1235 	static char headerType[MAX_VAR];
1236 	static char headerName[512];
1237 	listBlock_t* block;
1238 	const char* token;
1239 
1240 	if (!text) {
1241 		*lastList = 0;
1242 
1243 		/* free the old file */
1244 		if (lBuffer) {
1245 			FS_FreeFile(lBuffer);
1246 			lBuffer = nullptr;
1247 		}
1248 
1249 		return nullptr;
1250 	}
1251 
1252 	if (!Q_streq(files, lastList)) {
1253 		/* search for file lists */
1254 		Q_strncpyz(lastList, files, sizeof(lastList));
1255 
1256 		for (block = fs_blocklist; block; block = block->next) {
1257 			if (Q_streq(files, block->path))
1258 				break;
1259 		}
1260 
1261 		if (!block)
1262 			/* didn't find any valid file list */
1263 			return nullptr;
1264 
1265 		lBlock = block;
1266 		lFile = block->files;
1267 	}
1268 
1269 	while (lBlock) {
1270 		if (lBuffer) {
1271 			/* continue reading the current file */
1272 			if (*text) {
1273 				token = Com_Parse(text);
1274 				if (*token == '{') {
1275 					Com_SkipBlock(text);
1276 					continue;
1277 				}
1278 
1279 				Q_strncpyz(headerType, token, sizeof(headerType));
1280 				if (*text) {
1281 					token = Com_Parse(text);
1282 					Q_strncpyz(headerName, token, sizeof(headerName));
1283 					*name = headerName;
1284 					return headerType;
1285 				}
1286 			}
1287 
1288 			/* search a new file */
1289 			lFile = lFile->next;
1290 
1291 			while (!lFile && lBlock) {
1292 				/* it was the last file in the block, continue to next block */
1293 				for (lBlock = lBlock->next; lBlock; lBlock = lBlock->next) {
1294 					if (Q_streq(files, lBlock->path)) {
1295 						lFile = lBlock->files;
1296 						break;
1297 					}
1298 				}
1299 			}
1300 		}
1301 
1302 		if (lFile) {
1303 			char filename[MAX_QPATH];
1304 
1305 			/* free the old file */
1306 			if (lBuffer) {
1307 				FS_FreeFile(lBuffer);
1308 				lBuffer = nullptr;
1309 			}
1310 
1311 			/* load a new file */
1312 			Q_strncpyz(filename, lBlock->path, sizeof(filename));
1313 			strcpy(strrchr(filename, '/') + 1, (const char*)lFile->data);
1314 
1315 			FS_LoadFile(filename, &lBuffer);
1316 			/* skip a file that couldn't get loaded */
1317 			if (!lBuffer) {
1318 				lFile = lFile->next;
1319 				continue;
1320 			}
1321 			*text = (char*)lBuffer;
1322 		} else if (!lBuffer)
1323 			break;
1324 	}
1325 
1326 	/* free the old file */
1327 	if (lBuffer) {
1328 		FS_FreeFile(lBuffer);
1329 		lBuffer = nullptr;
1330 	}
1331 
1332 	/* finished */
1333 	return nullptr;
1334 }
1335 
1336 /* global vars for maplisting */
1337 char* fs_maps[MAX_MAPS];
1338 int fs_numInstalledMaps = -1;
1339 static bool fs_mapsInstalledInit = false;
1340 
1341 /**
1342  * @sa Com_MapDefSort
1343  */
FS_MapDefSort(const void * map1,const void * map2)1344 static int FS_MapDefSort (const void* map1, const void* map2)
1345 {
1346 	const char* mapStr1 = *(const char*  const *)map1;
1347 	const char* mapStr2 = *(const char*  const *)map2;
1348 
1349 	/* skip special map chars for rma and base attack */
1350 	if (mapStr1[0] == '+')
1351 		mapStr1++;
1352 	if (mapStr2[0] == '+')
1353 		mapStr2++;
1354 
1355 	return Q_StringSort(mapStr1, mapStr2);
1356 }
1357 
1358 /**
1359  * @brief Checks for valid BSP-file
1360  *
1361  * @param[in] filename BSP-file to check
1362  *
1363  * @return 0 if valid
1364  * @return 1 could not open file
1365  * @return 2 if magic number is bad
1366  * @return 3 if version of bsp-file is bad
1367  */
CheckBSPFile(const char * filename)1368 static int CheckBSPFile (const char* filename)
1369 {
1370 	/* load the file */
1371 	char name[MAX_QPATH];
1372 	Com_sprintf(name, sizeof(name), "maps/%s.bsp", filename);
1373 
1374 	ScopedFile file;
1375 	FS_OpenFile(name, &file, FILE_READ);
1376 	if (!file)
1377 		return 1;
1378 
1379 	int header[2];
1380 	FS_Read(header, sizeof(header), &file);
1381 
1382 	for (int i = 0; i < 2; i++)
1383 		header[i] = LittleLong(header[i]);
1384 
1385 	if (header[0] != IDBSPHEADER)
1386 		return 2;
1387 	if (header[1] != BSPVERSION)
1388 		return 3;
1389 
1390 	/* valid BSP-File */
1391 	return 0;
1392 }
1393 
1394 /**
1395  * @brief File the fs_maps array with valid maps
1396  * @param[in] reset If true the directory is scanned every time for new maps (useful for dedicated servers).
1397  * If false we only use the maps array (for clients e.g.)
1398  */
FS_GetMaps(bool reset)1399 void FS_GetMaps (bool reset)
1400 {
1401 	char findname[MAX_OSPATH];
1402 	char filename[MAX_QPATH];
1403 	int status, i;
1404 	const char* baseMapName = nullptr;
1405 	char** dirnames;
1406 	int ndirs;
1407 	searchpath_t* search;
1408 	pack_t* pak;
1409 
1410 	/* force a reread */
1411 	if (!reset && fs_mapsInstalledInit)
1412 		return;
1413 	else if (fs_mapsInstalledInit) {
1414 		for (i = 0; i <= fs_numInstalledMaps; i++)
1415 			Mem_Free(fs_maps[i]);
1416 	}
1417 
1418 	fs_numInstalledMaps = -1;
1419 
1420 	/* search through the path, one element at a time */
1421 	for (search = fs_searchpaths; search; search = search->next) {
1422 		/* is the element a pak file? */
1423 		if (search->pack) {
1424 			/* look through all the pak file elements */
1425 			pak = search->pack;
1426 			for (i = 0; i < pak->numfiles; i++) {
1427 				/* found it! */
1428 				baseMapName = strchr(pak->files[i].name, '/');
1429 				if (baseMapName) {
1430 					/** @todo paths are normalized here? */
1431 					baseMapName = strchr(baseMapName + 1, '/');
1432 					/* ugly hack - only show the maps in base/maps - not in base/maps/b and so on */
1433 					if (baseMapName)
1434 						continue;
1435 				} else
1436 					continue;
1437 
1438 				if (strstr(pak->files[i].name, ".bsp") || strstr(pak->files[i].name, ".ump") ) {
1439 					if (fs_numInstalledMaps + 1 >= MAX_MAPS) {
1440 						Com_Printf("FS_GetMaps: Max maps limit hit\n");
1441 						break;
1442 					}
1443 					fs_maps[fs_numInstalledMaps + 1] = Mem_PoolAllocTypeN(char, MAX_QPATH, com_fileSysPool);
1444 					if (fs_maps[fs_numInstalledMaps + 1] == nullptr) {
1445 						Com_Printf("Could not allocate memory in FS_GetMaps\n");
1446 						continue;
1447 					}
1448 					Q_strncpyz(findname, pak->files[i].name, sizeof(findname));
1449 					FS_NormPath(findname);
1450 					baseMapName = Com_SkipPath(findname);
1451 					Com_StripExtension(baseMapName, filename, sizeof(filename));
1452 					fs_numInstalledMaps++;
1453 					if (strstr(findname, ".ump"))
1454 						Com_sprintf(fs_maps[fs_numInstalledMaps], MAX_QPATH, "+%s", filename);
1455 					else
1456 						Q_strncpyz(fs_maps[fs_numInstalledMaps], filename, MAX_QPATH);
1457 				}
1458 			}
1459 		} else {
1460 			Com_sprintf(findname, sizeof(findname), "%s/maps/*.bsp", search->filename);
1461 			FS_NormPath(findname);
1462 
1463 			dirnames = FS_ListFiles(findname, &ndirs, 0, SFF_HIDDEN | SFF_SYSTEM);
1464 			if (dirnames != nullptr) {
1465 				for (i = 0; i < ndirs - 1; i++) {
1466 					baseMapName = Com_SkipPath(dirnames[i]);
1467 					Com_StripExtension(baseMapName, filename, sizeof(filename));
1468 					status = CheckBSPFile(filename);
1469 					if (!status) {
1470 						if (fs_numInstalledMaps + 1 >= MAX_MAPS) {
1471 							Com_Printf("FS_GetMaps: Max maps limit hit\n");
1472 							break;
1473 						}
1474 						fs_maps[fs_numInstalledMaps + 1] = Mem_PoolAllocTypeN(char, MAX_QPATH, com_fileSysPool);
1475 						if (fs_maps[fs_numInstalledMaps + 1] == nullptr) {
1476 							Com_Printf("Could not allocate memory in FS_GetMaps\n");
1477 							Mem_Free(dirnames[i]);
1478 							continue;
1479 						}
1480 						fs_numInstalledMaps++;
1481 						Q_strncpyz(fs_maps[fs_numInstalledMaps], filename, MAX_QPATH);
1482 					} else
1483 						Com_Printf("invalid mapstatus: %i (%s)\n", status, dirnames[i]);
1484 					Mem_Free(dirnames[i]);
1485 				}
1486 				Mem_Free(dirnames);
1487 			}
1488 			/* +RMA to maplisting */
1489 			Com_sprintf(findname, sizeof(findname), "%s/maps/*.ump", search->filename);
1490 			FS_NormPath(findname);
1491 
1492 			dirnames = FS_ListFiles(findname, &ndirs, 0, SFF_HIDDEN | SFF_SYSTEM);
1493 			if (dirnames != nullptr) {
1494 				for (i = 0; i < ndirs - 1; i++) {
1495 					baseMapName = Com_SkipPath(dirnames[i]);
1496 					Com_StripExtension(baseMapName, filename, sizeof(filename));
1497 					if (fs_numInstalledMaps + 1 >= MAX_MAPS) {
1498 						Com_Printf("FS_GetMaps: Max maps limit hit\n");
1499 						break;
1500 					}
1501 					fs_maps[fs_numInstalledMaps + 1] = Mem_PoolAllocTypeN(char, MAX_QPATH, com_fileSysPool);
1502 					if (fs_maps[fs_numInstalledMaps + 1] == nullptr) {
1503 						Com_Printf("Could not allocate memory in FS_GetMaps\n");
1504 						Mem_Free(dirnames[i]);
1505 						continue;
1506 					}
1507 					fs_numInstalledMaps++;
1508 					Com_sprintf(fs_maps[fs_numInstalledMaps], MAX_QPATH, "+%s", filename);
1509 					Mem_Free(dirnames[i]);
1510 				}
1511 				Mem_Free(dirnames);
1512 			}
1513 		}
1514 	}
1515 
1516 	fs_mapsInstalledInit = true;
1517 
1518 	qsort(fs_maps, fs_numInstalledMaps + 1, sizeof(char*), FS_MapDefSort);
1519 }
1520 
1521 /**
1522  * @brief Can print chunks for 1024 chars into a file.
1523  * @note The file must already be opened and may not be a zip file handle
1524  */
FS_Printf(qFILE * f,const char * msg,...)1525 int FS_Printf (qFILE *f, const char* msg, ...)
1526 {
1527 	va_list ap;
1528 	int len;
1529 	char buf[1024];
1530 
1531 	va_start(ap, msg);
1532 	Q_vsnprintf(buf, sizeof(buf), msg, ap);
1533 	len = fprintf(f->f, "%s", buf);
1534 	va_end(ap);
1535 
1536 	return len;
1537 }
1538 
1539 /**
1540  * @brief Properly handles partial writes
1541  */
FS_Write(const void * buffer,int len,qFILE * f)1542 int FS_Write (const void* buffer, int len, qFILE * f)
1543 {
1544 	if (!f->f)
1545 		return 0;
1546 
1547 	const byte* buf = (const byte*) buffer;
1548 
1549 	int remaining = len;
1550 	int tries = 0;
1551 	while (remaining) {
1552 		int block = remaining;
1553 		int written = fwrite(buf, 1, block, f->f);
1554 		if (written == 0) {
1555 			if (!tries) {
1556 				tries = 1;
1557 			} else {
1558 				Com_Printf("FS_Write: 0 bytes written\n");
1559 				return 0;
1560 			}
1561 		}
1562 
1563 		if (written == -1) {
1564 			Com_Printf("FS_Write: -1 bytes written\n");
1565 			return 0;
1566 		}
1567 
1568 		remaining -= written;
1569 		buf += written;
1570 	}
1571 	return len;
1572 }
1573 
1574 
FS_WriteFile(const void * buffer,size_t len,const char * filename)1575 int FS_WriteFile (const void* buffer, size_t len, const char* filename)
1576 {
1577 	ScopedFile f;
1578 	FS_OpenFile(filename, &f, FILE_WRITE);
1579 	if (!f)
1580 		return 0;
1581 
1582 	const int c = FS_Write(buffer, len, &f);
1583 	const int lencheck = FS_FileLength(&f);
1584 
1585 	/* if file write failed (file is incomplete) then delete it */
1586 	if (c != len || lencheck != len) {
1587 		Com_Printf("FS_WriteFile: failed to finish writing '%s'\n", filename);
1588 		if (remove(va("%s/%s", FS_Gamedir(), filename)))
1589 			Com_Printf("FS_WriteFile: could not remove file: %s\n", filename);
1590 		return 0;
1591 	}
1592 
1593 	return c;
1594 }
1595 
1596 /**
1597  * @brief Return current working dir
1598  */
FS_GetCwd(void)1599 const char* FS_GetCwd (void)
1600 {
1601 	static char buf[MAX_OSPATH];
1602 	Q_strncpyz(buf, Sys_Cwd(), sizeof(buf));
1603 	FS_NormPath(buf);
1604 	return buf;
1605 }
1606 
1607 /**
1608  * @brief Checks whether a file exists (not in virtual filesystem)
1609  * @sa FS_CheckFile
1610  * @param[in] filename Full filesystem path to the file
1611  */
FS_FileExists(const char * filename,...)1612 bool FS_FileExists (const char* filename, ...)
1613 {
1614 	char path[MAX_OSPATH];
1615 	va_list ap;
1616 
1617 	va_start(ap, filename);
1618 	Q_vsnprintf(path, sizeof(path), filename, ap);
1619 	va_end(ap);
1620 
1621 #ifdef _WIN32
1622 	return (_access(path, 00) == 0);
1623 #else
1624 	return (access(path, R_OK) == 0);
1625 #endif
1626 }
1627 
1628 /**
1629  * @brief Cleanup function
1630  * @sa FS_InitFilesystem
1631  * @sa FS_RestartFilesystem
1632  */
FS_Shutdown(void)1633 void FS_Shutdown (void)
1634 {
1635 	searchpath_t* p, *next;
1636 
1637 	if (fs_openedFiles != 0) {
1638 		Com_Printf("There are still %i opened files\n", fs_openedFiles);
1639 	}
1640 
1641 	/* free everything */
1642 	for (p = fs_searchpaths; p; p = next) {
1643 		next = p->next;
1644 
1645 		if (p->pack) {
1646 			unzClose(p->pack->handle.z);
1647 			Mem_Free(p->pack->files);
1648 			Mem_Free(p->pack);
1649 		}
1650 		Mem_Free(p);
1651 	}
1652 
1653 	/* any FS_ calls will now be an error until reinitialized */
1654 	fs_searchpaths = nullptr;
1655 	fs_links = nullptr;
1656 	fs_mapsInstalledInit = false;
1657 	fs_numInstalledMaps = -1;
1658 	fs_blocklist = nullptr;
1659 
1660 #ifdef COMPILE_UFO
1661 	FS_RemoveCommands();
1662 #endif
1663 
1664 	Mem_FreePool(com_fileSysPool);
1665 }
1666 
1667 /**
1668  * @brief Restart the filesystem (reload all pk3 files)
1669  * @note Call this after you finished a download
1670  * @sa FS_Shutdown
1671  * @sa FS_InitFilesystem
1672  */
FS_RestartFilesystem(const char * gamedir)1673 void FS_RestartFilesystem (const char* gamedir)
1674 {
1675 	if (gamedir != nullptr)
1676 		Com_Printf("restarting with gamedir set to %s\n", gamedir);
1677 	throw comRestart_t(gamedir);
1678 }
1679 
1680 /**
1681  * @brief Copy a fully specified file from one place to another
1682  * @todo Allow copy of pk3 file content
1683  */
FS_CopyFile(const char * fromOSPath,const char * toOSPath)1684 void FS_CopyFile (const char* fromOSPath, const char* toOSPath)
1685 {
1686 	FILE *f;
1687 	int len;
1688 
1689 	if (!fs_searchpaths)
1690 		Sys_Error("Filesystem call made without initialization");
1691 
1692 	Com_Printf("FS_CopyFile: copy %s to %s\n", fromOSPath, toOSPath);
1693 
1694 	f = fopen(fromOSPath, "rb");
1695 	if (!f)
1696 		return;
1697 
1698 	fseek(f, 0, SEEK_END);
1699 	len = ftell(f);
1700 	fseek(f, 0, SEEK_SET);
1701 
1702 	byte* const buf = Mem_PoolAllocTypeN(byte, len, com_fileSysPool);
1703 	if (fread(buf, 1, len, f) != len)
1704 		Sys_Error("Short read in FS_CopyFile");
1705 	fclose(f);
1706 
1707 	FS_CreatePath(toOSPath);
1708 
1709 	f = fopen(toOSPath, "wb");
1710 	if (!f) {
1711 		Mem_Free(buf);
1712 		return;
1713 	}
1714 
1715 	if (fwrite(buf, 1, len, f) != len)
1716 		Sys_Error("Short write in FS_CopyFile");
1717 
1718 	fclose(f);
1719 	Mem_Free(buf);
1720 }
1721 
1722 /**
1723  * @sa FS_CopyFile
1724  */
FS_RemoveFile(const char * osPath)1725 void FS_RemoveFile (const char* osPath)
1726 {
1727 	if (!fs_searchpaths)
1728 		Sys_Error("Filesystem call made without initialization");
1729 
1730 	Com_Printf("FS_RemoveFile: remove %s\n", osPath);
1731 	remove(osPath);
1732 }
1733 
1734 /**
1735  * @brief Renames a file
1736  * @sa FS_RemoveFile
1737  * @sa FS_CopyFile
1738  * @param[in] from The source filename
1739  * @param[in] to The filename we want after the rename
1740  * @param[in] relative If relative is true we have to add the FS_Gamedir path for writing
1741  */
FS_RenameFile(const char * from,const char * to,bool relative)1742 bool FS_RenameFile (const char* from, const char* to, bool relative)
1743 {
1744 	char from_buf[MAX_OSPATH];
1745 	char to_buf[MAX_OSPATH];
1746 
1747 	if (!fs_searchpaths)
1748 		Sys_Error("Filesystem call made without initialization");
1749 
1750 	if (relative) {
1751 		Com_sprintf(from_buf, sizeof(from_buf), "%s/%s", FS_Gamedir(), from);
1752 		Com_sprintf(to_buf, sizeof(to_buf), "%s/%s", FS_Gamedir(), to);
1753 		from = from_buf;
1754 		to = to_buf;
1755 	}
1756 
1757 	return rename(from, to) == 0;
1758 }
1759