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