/* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "qwsvdef.h" /* All of Quake's data access is through a hierchal file system, but the contents of the file system can be transparently merged from several sources. The "base directory" is the path to the directory holding the quake.exe and all game directories. The sys_* files pass this to host_init in quakeparms_t->basedir. This can be overridden with the "-basedir" command line parm to allow code debugging in a different directory. The base directory is only used during filesystem initialization. The "game directory" is the first tree on the search path and directory that all generated files (savegames, screenshots, demos, config files) will be saved to. This can be overridden with the "-game" command line parameter. The game directory can never be changed while quake is executing. This is a precaution against having a malicious server instruct clients to write files over areas they shouldn't. */ typedef enum { FS_LOAD_NONE = 1, FS_LOAD_FILE_PAK = 2, FS_LOAD_FILE_ALL = FS_LOAD_FILE_PAK } FS_Load_File_Types; /* ============================================================================= VARIABLES ============================================================================= */ // WARNING: if you add some FS related global variable // then made appropriate change to FS_ShutDown() too, if required. static char fs_basedir[MAX_OSPATH]; // c:/quake char fs_gamedir[MAX_OSPATH]; // c:/quake/qw static char fs_gamedirfile[MAX_QPATH]; // qw tf ctf and etc. In other words single dir name without path static searchpath_t *fs_searchpaths = NULL; static searchpath_t *fs_base_searchpaths = NULL; // without gamedirs hashtable_t *filesystemhash = NULL; qbool filesystemchanged = true; int fs_hash_dups = 0; int fs_hash_files = 0; cvar_t fs_cache = {"fs_cache", "1"}; /* ============================================================================= FORWARD DEFINITION ============================================================================= */ static searchpath_t *FS_AddPathHandle(char *probablepath, searchpathfuncs_t *funcs, void *handle, qbool copyprotect, qbool istemporary, FS_Load_File_Types loadstuff); /* ============================================================================= COMMANDS ============================================================================= */ /* ============ FS_Path_f ============ */ static void FS_Path_f (void) { searchpath_t *search; Con_Printf ("Current search path:\n"); for (search = fs_searchpaths; search ; search = search->next) { if (search == fs_base_searchpaths) Con_Printf ("----------\n"); search->funcs->PrintPath(search->handle); } } /* ============================================================================= FUNCTIONS ============================================================================= */ /* ================ FS_GetCleanPath ================ */ static const char *FS_GetCleanPath(const char *pattern, char *outbuf, int outlen) { char *s; if (strchr(pattern, '\\')) { strlcpy(outbuf, pattern, outlen); pattern = outbuf; Con_Printf("Warning: \\ characters in filename %s\n", pattern); for (s = (char*)pattern; (s = strchr(s, '\\')); s++) *s = '/'; } if (*pattern == '/' || strstr(pattern, "..") || strstr(pattern, ":")) Con_Printf("Error: absolute path in filename %s\n", pattern); else return pattern; return NULL; } /* ============ FS_FlushFSHash Flush FS hash and mark FS as changed, so FS_FLocateFile() will be forced to call FS_RebuildFSHash(). ============ */ void FS_FlushFSHash(void) { if (filesystemhash) { Hash_Flush(filesystemhash); } filesystemchanged = true; } /* ============ FS_RebuildFSHash Rebuild FS hash. ============ */ static void FS_RebuildFSHash(void) { searchpath_t *search; if (!filesystemhash) { filesystemhash = Hash_InitTable(1024); } else { FS_FlushFSHash(); } fs_hash_dups = 0; fs_hash_files = 0; for (search = fs_searchpaths ; search ; search = search->next) { search->funcs->BuildHash(search->handle); } filesystemchanged = false; Con_DPrintf("FS_RebuildFSHash: %i unique files, %i duplicates\n", fs_hash_files, fs_hash_dups); } /* ============ FS_FLocateFile Finds the file in the search path. Look FSLF_ReturnType_e definition so you know that it returns. ============ */ int FS_FLocateFile(const char *filename, FSLF_ReturnType_e returntype, flocation_t *loc) { int depth = 0; int len; searchpath_t *search; char cleanpath[MAX_OSPATH]; void *pf = NULL; filename = FS_GetCleanPath(filename, cleanpath, sizeof(cleanpath)); if (!filename) goto fail; if (fs_cache.value) { if (filesystemchanged) FS_RebuildFSHash(); pf = Hash_GetInsensitive(filesystemhash, filename); if (!pf) goto fail; } // // search through the path, one element at a time. // for (search = fs_searchpaths ; search ; search = search->next) { if (search->funcs->FindFile(search->handle, loc, filename, pf)) { if (loc) { loc->search = search; len = loc->len; } else { len = 0; } goto out; } depth += (search->funcs == &osfilefuncs || returntype == FSLFRT_DEPTH_ANYPATH); } fail: if (loc) loc->search = NULL; depth = 0x7fffffff; // NOTE: weird, we return it on fail in some cases, may cause mistakes by user. len = -1; out: /* Debug printing removed * if (len>=0) { if (loc) Con_Printf("Found %s:%i\n", loc->rawname, loc->len); else Con_Printf("Found %s\n", filename); } else Con_Printf("Failed\n"); */ if (returntype == FSLFRT_IFFOUND) return len != -1; else if (returntype == FSLFRT_LENGTH) return len; else return depth; } // internal struct, no point to expose it outside. typedef struct { searchpathfuncs_t *funcs; searchpath_t *parentpath; char *parentdesc; } wildpaks_t; /* ================ FS_AddWildDataFiles Add pack files masked by wildcards, e.g. '*.pak' . That allow add not only packs named like pak0.pak pak1.pak but with random names too. ================ */ static int FS_AddWildDataFiles (char *descriptor, int size, void *vparam) { wildpaks_t *param = vparam; vfsfile_t *vfs; searchpathfuncs_t *funcs = param->funcs; searchpath_t *search; void *pak; char pakfile[MAX_OSPATH]; flocation_t loc = {0}; snprintf (pakfile, sizeof (pakfile), "%s%s", param->parentdesc, descriptor); for (search = fs_searchpaths; search; search = search->next) { if (search->funcs != funcs) continue; if (!strcasecmp((char*)search->handle, pakfile)) //assumption: first member of structure is a char array return true; //already loaded (base paths?) } search = param->parentpath; if (!search->funcs->FindFile(search->handle, &loc, descriptor, NULL)) return true; //not found.. vfs = search->funcs->OpenVFS(search->handle, &loc, "rb"); if (!vfs) return true; pak = funcs->OpenNew (vfs, pakfile); if (!pak) { VFS_CLOSE(vfs); return true; } snprintf (pakfile, sizeof (pakfile), "%s%s/", param->parentdesc, descriptor); FS_AddPathHandle(pakfile, funcs, pak, true, false, FS_LOAD_FILE_ALL); return true; } /* ================ FS_AddDataFiles Load pack files. ================ */ static void FS_AddDataFiles(char *pathto, searchpath_t *parent, char *extension, searchpathfuncs_t *funcs) { int i; char pakfile[MAX_OSPATH]; wildpaks_t wp = {0}; // first load all the numbered pak files. for (i = 0; ; i++) { void *handle; vfsfile_t *vfs; flocation_t loc = {0}; snprintf (pakfile, sizeof(pakfile), "pak%i.%s", i, extension); if (!parent->funcs->FindFile(parent->handle, &loc, pakfile, NULL)) break; //not found.. snprintf (pakfile, sizeof(pakfile), "%spak%i.%s", pathto, i, extension); vfs = parent->funcs->OpenVFS(parent->handle, &loc, "rb"); if (!vfs) break; handle = funcs->OpenNew (vfs, pakfile); if (!handle) { VFS_CLOSE(vfs); break; } snprintf (pakfile, sizeof(pakfile), "%spak%i.%s/", pathto, i, extension); FS_AddPathHandle(pakfile, funcs, handle, true, false, FS_LOAD_FILE_ALL); } // now load the random ones. snprintf (pakfile, sizeof (pakfile), "*.%s", extension); wp.funcs = funcs; wp.parentdesc = pathto; wp.parentpath = parent; parent->funcs->EnumerateFiles(parent->handle, pakfile, FS_AddWildDataFiles, &wp); } /* ================ FS_AddPathHandle Adds searchpath and load pack files in it if requested. ================ */ static searchpath_t *FS_AddPathHandle(char *probablepath, searchpathfuncs_t *funcs, void *handle, qbool copyprotect, qbool istemporary, FS_Load_File_Types loadstuff) { searchpath_t *search; // allocate new search path and init it. search = (searchpath_t*)Q_malloc (sizeof(searchpath_t)); search->copyprotected = copyprotect; search->istemporary = istemporary; search->handle = handle; search->funcs = funcs; // link seach path in. search->next = fs_searchpaths; fs_searchpaths = search; // mark file system is changed. filesystemchanged = true; // add any data files too. if (loadstuff & FS_LOAD_FILE_PAK) FS_AddDataFiles(probablepath, search, "pak", &packfilefuncs);//q1/hl/h2/q2 return search; } /* ================ FS_AddGameDirectory Sets fs_gamedir, adds the directory to the head of the path, then loads and adds pak0.pak pak1.pak ... ================ */ static void FS_AddGameDirectory (char *dir, FS_Load_File_Types loadstuff) { char *p; searchpath_t *search; if ((p = strrchr(dir, '/'))) strlcpy (fs_gamedirfile, ++p, sizeof (fs_gamedirfile)); else strlcpy (fs_gamedirfile, dir, sizeof (fs_gamedirfile)); strlcpy (fs_gamedir, dir, sizeof (fs_gamedir)); for (search = fs_searchpaths; search; search = search->next) { if (search->funcs != &osfilefuncs) continue; // ignore packs and such. if (!strcasecmp(search->handle, fs_gamedir)) return; //already loaded (base paths?) } // add the directory to the search path FS_AddPathHandle (va("%s/", dir), &osfilefuncs, Q_strdup(dir), false, false, loadstuff); } /* ============ FS_SetGamedir Sets the gamedir and path to a different directory. That is basically handy wrapper around FS_AddGameDirectory() for "gamedir" command. ============ */ void FS_SetGamedir (char *dir, qbool force) { if (strstr(dir, "..") || strstr(dir, "/") || strstr(dir, "\\") || strstr(dir, ":")) { Con_Printf ("Gamedir should be a single filename, not a path\n"); return; } if (!force && !strcmp(fs_gamedirfile, dir)) return; // Still the same, unless we forced. // FIXME: do we need it? since it will be set in FS_AddGameDirectory(). strlcpy (fs_gamedirfile, dir, sizeof(fs_gamedirfile)); // Free up any current game dir info. FS_FlushFSHash(); // free up any current game dir info while (fs_searchpaths != fs_base_searchpaths) { searchpath_t *next; fs_searchpaths->funcs->ClosePath(fs_searchpaths->handle); next = fs_searchpaths->next; Q_free (fs_searchpaths); fs_searchpaths = next; } // mark file system is changed. filesystemchanged = true; // Flush all data, so it will be forced to reload. // well, mvdsv does not use cache anyway. // Cache_Flush (); // FIXME: do we need it? since it will be set in FS_AddGameDirectory(). snprintf (fs_gamedir, sizeof (fs_gamedir), "%s/%s", fs_basedir, dir); FS_AddGameDirectory(va("%s/%s", fs_basedir, dir), FS_LOAD_FILE_ALL); } /* ================ FS_InitModule Add commands and cvars. ================ */ void FS_InitModule(void) { Cmd_AddCommand ("path", FS_Path_f); Cvar_Register(&fs_cache); } /* ================ FS_Init ================ */ void FS_InitEx(void) { int i; char *s; FS_ShutDown(); if ((i = COM_CheckParm ("-basedir")) && i < COM_Argc() - 1) { // -basedir // Overrides the system supplied base directory (under id1) strlcpy (fs_basedir, COM_Argv(i + 1), sizeof(fs_basedir)); } else { #if 0 // FIXME: made fs_basedir equal to cwd Sys_getcwd(fs_basedir, sizeof(fs_basedir) - 1); #else strlcpy (fs_basedir, ".", sizeof(fs_basedir)); #endif } // replace backslahes with slashes. for (s = fs_basedir; (s = strchr(s, '\\')); s++) *s = '/'; // remove terminating slash if any. i = (int)strlen(fs_basedir) - 1; if (i >= 0 && fs_basedir[i] == '/') fs_basedir[i] = 0; // start up with id1 by default FS_AddGameDirectory(va("%s/%s", fs_basedir, "id1"), FS_LOAD_FILE_ALL); FS_AddGameDirectory(va("%s/%s", fs_basedir, "qw"), FS_LOAD_FILE_ALL); // any set gamedirs will be freed up to here fs_base_searchpaths = fs_searchpaths; // the user might want to override default game directory if (!(i = COM_CheckParm ("-game"))) i = COM_CheckParm ("+gamedir"); if (i && i < COM_Argc() - 1) { FS_SetGamedir (COM_Argv(i + 1), true); // FIXME: move in FS_SetGamedir() instead!!! Info_SetValueForStarKey (svs.info, "*gamedir", COM_Argv(i + 1), MAX_SERVERINFO_STRING); } } /* ================ FS_Init ================ */ void FS_Init( void ) { FS_InitModule(); // init commands and variables. FS_InitEx(); } /* ================ FS_ShutDown ================ */ void FS_ShutDown( void ) { // free data while (fs_searchpaths) { searchpath_t *next = fs_searchpaths->next; Q_free (fs_searchpaths); // FIXME: close handles and such!!! fs_searchpaths = next; } // flush all data, so it will be forced to reload // well, mvdsv does not use cache anyway. // Cache_Flush (); // reset globals fs_base_searchpaths = fs_searchpaths = NULL; fs_gamedir[0] = 0; fs_gamedirfile[0] = 0; fs_basedir[0] = 0; // FIXME: //hashtable_t *filesystemhash = NULL; //qbool filesystemchanged = true; //int fs_hash_dups = 0; //int fs_hash_files = 0; } /* ================ FS_OpenVFS This should be how all files are opened. ================ */ vfsfile_t *FS_OpenVFS(const char *filename, char *mode, relativeto_t relativeto) { flocation_t loc = {0}; vfsfile_t *vfs = NULL; char cleanname[MAX_OSPATH]; char fullname[MAX_OSPATH]; //blanket-bans filename = FS_GetCleanPath(filename, cleanname, sizeof(cleanname)); if (!filename) return NULL; if (strcmp(mode, "rb")) if (strcmp(mode, "wb")) if (strcmp(mode, "ab")) return NULL; //urm, unable to write/append /* General opening of files */ switch (relativeto) { case FS_NONE_OS: //OS access only, no paks, open file as is snprintf(fullname, sizeof(fullname), "%s", filename); return VFSOS_Open(fullname, mode); case FS_GAME_OS: //OS access only, no paks snprintf(fullname, sizeof(fullname), "%s/%s/%s", fs_basedir, fs_gamedirfile, filename); if (strchr(mode, 'w') || strchr(mode, 'a')) FS_CreatePath(fullname); // FIXME: It should be moved to VFSOS_Open() itself? return VFSOS_Open(fullname, mode); case FS_GAME: // snprintf(fullname, sizeof(fullname), "%s/%s/%s", fs_basedir, fs_gamedirfile, filename); // That an error attempt to write with FS_GAME, since file can be in pack file. redirect. if (strchr(mode, 'w') || strchr(mode, 'a')) return FS_OpenVFS(filename, mode, FS_GAME_OS); // Search on path, try to open if found. if (FS_FLocateFile(filename, FSLFRT_IFFOUND, &loc)) return loc.search->funcs->OpenVFS(loc.search->handle, &loc, mode); return NULL; case FS_BASE_OS: //OS access only, no paks snprintf(fullname, sizeof(fullname), "%s/%s", fs_basedir, filename); return VFSOS_Open(fullname, mode); case FS_ANY: // That an error attempt to write with FS_ANY, since file can be in pack file. vfs = FS_OpenVFS(filename, mode, FS_GAME); if (vfs) return vfs; vfs = FS_OpenVFS(filename, mode, FS_NONE_OS); if (vfs) return vfs; return NULL; default: Sys_Error("FS_OpenVFS: Bad relative path (%i)", relativeto); break; } return NULL; } //======================================================================= void VFS_CHECKCALL (struct vfsfile_s *vf, void *fld, char *emsg) { if (!fld) Sys_Error("%s", emsg); } void VFS_CLOSE (struct vfsfile_s *vf) { assert(vf); VFS_CHECKCALL(vf, vf->Close, "VFS_CLOSE"); vf->Close(vf); } unsigned long VFS_TELL (struct vfsfile_s *vf) { assert(vf); VFS_CHECKCALL(vf, vf->Tell, "VFS_TELL"); return vf->Tell(vf); } unsigned long VFS_GETLEN (struct vfsfile_s *vf) { assert(vf); VFS_CHECKCALL(vf, vf->GetLen, "VFS_GETLEN"); return vf->GetLen(vf); } /** * VFS_SEEK() reposition a stream * If whence is set to SEEK_SET, SEEK_CUR, or SEEK_END, the offset is * relative to the start of the file, the current position indicator, or * end-of-file, respectively. * Return Value * Upon successful completion, VFS_SEEK(), returns 0. * Otherwise, -1 is returned */ int VFS_SEEK (struct vfsfile_s *vf, unsigned long pos, int whence) { assert(vf); VFS_CHECKCALL(vf, vf->Seek, "VFS_SEEK"); return vf->Seek(vf, pos, whence); } int VFS_READ (struct vfsfile_s *vf, void *buffer, int bytestoread, vfserrno_t *err) { assert(vf); VFS_CHECKCALL(vf, vf->ReadBytes, "VFS_READ"); return vf->ReadBytes(vf, buffer, bytestoread, err); } int VFS_WRITE (struct vfsfile_s *vf, const void *buffer, int bytestowrite) { assert(vf); VFS_CHECKCALL(vf, vf->WriteBytes, "VFS_WRITE"); return vf->WriteBytes(vf, buffer, bytestowrite); } void VFS_FLUSH (struct vfsfile_s *vf) { assert(vf); if(vf->Flush) vf->Flush(vf); } // return null terminated string char *VFS_GETS(struct vfsfile_s *vf, char *buffer, int buflen) { char in; char *out = buffer; int len = buflen-1; assert(vf); VFS_CHECKCALL(vf, vf->ReadBytes, "VFS_GETS"); // if (len == 0) // return NULL; // FIXME: I am not sure how to handle this better if (len <= 0) Sys_Error("VFS_GETS: len <= 0"); while (len > 0) { if (!VFS_READ(vf, &in, 1, NULL)) { if (len == buflen-1) return NULL; *out = '\0'; return buffer; } if (in == '\n') break; *out++ = in; len--; } *out = '\0'; return buffer; } qbool VFS_COPYPROTECTED(struct vfsfile_s *vf) { assert(vf); return vf->copyprotected; } /* ============================================================================= LEGACY FUNCTIONS ============================================================================= */ /* ================ FS_FileLength ================ */ long FS_FileLength (FILE *f) { long pos, end; pos = ftell (f); fseek (f, 0, SEEK_END); end = ftell (f); fseek (f, pos, SEEK_SET); return end; } /* ============ FS_FileBase ============ */ void FS_FileBase (char *in, char *out) { char *begin, *end; int len; if (!(end = strrchr (in, '.'))) end = in + strlen (in); if (!(begin = strchr (in, '/'))) begin = in; else begin++; len = end - begin + 1; if (len < 1) strlcpy (out, "?model?", 8); else strlcpy (out, begin, min (len, MAX_OSPATH)); } /* ============ FS_WriteFile The filename will be prefixed by the current game directory ============ */ void FS_WriteFile (char *filename, void *data, int len) { FILE *f; char name[MAX_OSPATH]; snprintf (name, MAX_OSPATH, "%s/%s", fs_gamedir, filename); f = fopen (name, "wb"); if (!f) { Sys_mkdir (fs_gamedir); f = fopen (name, "wb"); if (!f) Sys_Error ("Error opening %s", filename); } Sys_Printf ("FS_WriteFile: %s\n", name); fwrite (data, 1, len, f); fclose (f); } /* ============ FS_CreatePath Only used for CopyFile and download ============ */ void FS_CreatePath(char *path) { char *s, save; if (!*path) return; for (s = path + 1; *s; s++) { #ifdef _WIN32 if (*s == '/' || *s == '\\') { #else if (*s == '/') { #endif save = *s; *s = 0; Sys_mkdir(path); *s = save; } } } /* ============ FS_LoadFile Filename are relative to the quake directory. Always appends a 0 byte to the loaded data. ============ */ static byte *FS_LoadFile (char *path, void *allocator, int *file_length) { vfsfile_t *f = NULL; vfserrno_t err; flocation_t loc = {0}; byte *buf; int len; // Look for it in the filesystem or pack files. FS_FLocateFile(path, FSLFRT_LENGTH, &loc); if (loc.search) { f = loc.search->funcs->OpenVFS(loc.search->handle, &loc, "rb"); } if (!f) return NULL; // FIXME: should we use loc.len instead? len = VFS_GETLEN(f); if (file_length) *file_length = len; if (allocator == Hunk_AllocName) { char base[32]; // Extract the filename base name for hunk tag. FS_FileBase (path, base); buf = (byte *) Hunk_AllocName (len + 1, base); } else if (allocator == Hunk_TempAlloc) { buf = (byte *) Hunk_TempAlloc (len + 1); } #if 0 else if (allocator == Q_malloc) { buf = Q_malloc (len + 1); } #endif else { Sys_Error ("FS_LoadFile: bad usehunk\n"); return NULL; } if (!buf) { Sys_Error ("FS_LoadFile: not enough space for %s\n", path); return NULL; } buf[len] = 0; VFS_READ(f, buf, len, &err); VFS_CLOSE(f); return buf; } byte *FS_LoadHunkFile (char *path, int *len) { return FS_LoadFile (path, Hunk_AllocName, len); } byte *FS_LoadTempFile (char *path, int *len) { return FS_LoadFile (path, Hunk_TempAlloc, len); } /* ============ FS_NextPath Iterate along searchpaths (no packs). ============ */ char *FS_NextPath (char *prevpath) { searchpath_t *s; char *prev; if (!prevpath) return fs_gamedir; prev = fs_gamedir; for (s = fs_searchpaths; s ; s = s->next) { if (s->funcs != &osfilefuncs) continue; if (prevpath == prev) return s->handle; prev = s->handle; } return NULL; } /* =========== FS_UnsafeFilename Returns true if user-specified path is unsafe =========== */ qbool FS_UnsafeFilename(const char* fileName) { return !fileName || !*fileName || // invalid name. fileName[1] == ':' || // dos filename absolute path specified - reject. *fileName == '\\' || *fileName == '/' || // absolute path was given - reject. strstr(fileName, ".."); }