From a106924c7fe796a37fefd35eed8a944f21a8c89b Mon Sep 17 00:00:00 2001 From: Alibek Omarov Date: Sat, 9 May 2026 16:56:58 +0500 Subject: [PATCH] filesystem: avoid scanning game directory as executable in FS_FindLibrary on Android, generic refactoring --- filesystem/VFileSystem009.cpp | 3 - filesystem/filesystem.c | 323 ++++++++++++++++++------------- filesystem/filesystem.h | 19 +- filesystem/filesystem_internal.h | 9 +- 4 files changed, 199 insertions(+), 155 deletions(-) diff --git a/filesystem/VFileSystem009.cpp b/filesystem/VFileSystem009.cpp index 1d6fad64..3ef0946e 100644 --- a/filesystem/VFileSystem009.cpp +++ b/filesystem/VFileSystem009.cpp @@ -12,13 +12,10 @@ 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. */ -#include -#include #include #include #include ALLOCA_H #include "crtlib.h" -#include "filesystem.h" #include "filesystem_internal.h" #include "VFileSystem009.h" #include "common/com_strings.h" diff --git a/filesystem/filesystem.c b/filesystem/filesystem.c index 5e008a09..636f6a05 100644 --- a/filesystem/filesystem.c +++ b/filesystem/filesystem.c @@ -1254,6 +1254,32 @@ static qboolean FS_ParseGameInfo( const char *gamedir, gameinfo_t *GameInfo, qbo return false; } +/* +================ +FS_PathExecFlag + +guess if path is executable +only implemented for Android for now +================ +*/ +static uint32_t FS_PathExecFlag( const char *dir ) +{ +#if XASH_ANDROID + // on Android, where directories usually lie outside of internal app directory + // we can't reliably load libraries from + // + // currently, launcher passes /data/... path as rodir where downloaded game libraries + // are stored in. Catch that and mark such path as executable + if( !Q_strncmp( dir, "/data/", 6 )) + return FS_EXEC_PATH; + return 0; +#else // !XASH_ANDROID + // FIXME: read noexec flag on *nix systems? + (void)dir; + return FS_EXEC_PATH; +#endif // !XASH_ANDROID +} + /* ================ FS_AddGameHierarchy @@ -1261,9 +1287,8 @@ FS_AddGameHierarchy */ void FS_AddGameHierarchy( const char *dir, uint flags ) { - int i; - qboolean isGameDir = flags & FS_GAMEDIR_PATH; - char buf[MAX_VA_STRING]; + const qboolean is_game_dir = FBitSet( flags, FS_GAMEDIR_PATH ); + const uint32_t mount_flags = FBitSet( flags, FS_MOUNT_FLAGS ); if( COM_StringEmptyOrNULL( dir )) return; @@ -1274,7 +1299,7 @@ void FS_AddGameHierarchy( const char *dir, uint flags ) // recursive gamedirs // for example, czeror->czero->cstrike->valve - for( i = 0; i < FI.numgames; i++ ) + for( int i = 0; i < FI.numgames; i++ ) { if( !Q_stricmp( FI.games[i]->gamefolder, dir )) { @@ -1299,55 +1324,67 @@ void FS_AddGameHierarchy( const char *dir, uint flags ) } } + // clear flags not applicable to searchpaths + ClearBits( flags, FS_MOUNT_FLAGS ); + + char buf[MAX_VA_STRING]; if( !COM_StringEmpty( fs_rodir )) { - // append new flags to rodir, except FS_GAMEDIR_PATH and FS_CUSTOM_PATH - uint new_flags = FS_NOWRITE_PATH | (flags & (~FS_GAMEDIR_PATH|FS_CUSTOM_PATH)); - if( isGameDir ) - SetBits( new_flags, FS_GAMERODIR_PATH ); + uint32_t new_flags = flags + | FS_NOWRITE_PATH + | ( is_game_dir ? FS_GAMERODIR_PATH : 0 ) + | FS_PathExecFlag( fs_rodir ); + + // clear flags not applicable to read-only directory + ClearBits( new_flags, FS_GAMEDIR_PATH | FS_CUSTOM_PATH ); + + Q_snprintf( buf, sizeof( buf ), "%s/%s/", fs_rodir, dir ); FS_AllowDirectPaths( true ); - Q_snprintf( buf, sizeof( buf ), "%s/%s/", fs_rodir, dir ); FS_AddGameDirectory( buf, new_flags ); FS_AllowDirectPaths( false ); } - if( isGameDir ) + if( is_game_dir ) { Q_snprintf( buf, sizeof( buf ), "%s" DEFAULT_DOWNLOADED_DIRECTORY_SUFFIX "/", dir ); - FS_AddGameDirectory( buf, FS_NOWRITE_PATH|FS_CUSTOM_PATH ); + FS_AddGameDirectory( buf, flags | FS_NOWRITE_PATH | FS_CUSTOM_PATH ); } - Q_snprintf( buf, sizeof( buf ), "%s/", dir ); - FS_AddGameDirectory( buf, flags ); - if( FBitSet( flags, FS_MOUNT_HD )) + Q_snprintf( buf, sizeof( buf ), "%s/", dir ); + FS_AddGameDirectory( buf, flags | FS_PathExecFlag( buf ) | FS_PathExecFlag( fs_rootdir )); + + // paths after can only be addon paths + SetBits( flags, FS_NOWRITE_PATH | FS_CUSTOM_PATH ); + + if( FBitSet( mount_flags, FS_MOUNT_HD )) { Q_snprintf( buf, sizeof( buf ), "%s_hd/", dir ); - FS_AddGameDirectory( buf, flags|FS_NOWRITE_PATH|FS_CUSTOM_PATH ); + FS_AddGameDirectory( buf, flags ); } - if( FBitSet( flags, FS_MOUNT_ADDON )) + if( FBitSet( mount_flags, FS_MOUNT_ADDON )) { Q_snprintf( buf, sizeof( buf ), "%s_addon/", dir ); - FS_AddGameDirectory( buf, flags|FS_NOWRITE_PATH|FS_CUSTOM_PATH ); + FS_AddGameDirectory( buf, flags ); } - if( FBitSet( flags, FS_MOUNT_LV )) + if( FBitSet( mount_flags, FS_MOUNT_LV )) { Q_snprintf( buf, sizeof( buf ), "%s_lv/", dir ); - FS_AddGameDirectory( buf, flags|FS_NOWRITE_PATH|FS_CUSTOM_PATH ); + FS_AddGameDirectory( buf, flags ); } - if( FBitSet( flags, FS_MOUNT_L10N ) && !COM_StringEmpty( fs_language ) && Q_isalpha( fs_language )) + if( FBitSet( mount_flags, FS_MOUNT_L10N )) { Q_snprintf( buf, sizeof( buf ), "%s_%s/", dir, fs_language ); - FS_AddGameDirectory( buf, flags|FS_NOWRITE_PATH|FS_CUSTOM_PATH ); + FS_AddGameDirectory( buf, flags ); } - if( isGameDir ) + if( is_game_dir ) { Q_snprintf( buf, sizeof( buf ), "%s/" DEFAULT_CUSTOM_DIRECTORY, dir ); - FS_AddGameDirectory( buf, FS_NOWRITE_PATH|FS_CUSTOM_PATH ); + FS_AddGameDirectory( buf, flags ); } } @@ -1358,28 +1395,34 @@ FS_Rescan */ void FS_Rescan( uint32_t flags, const char *language ) { - const char *str; - Con_Reportf( "%s( %s )\n", __func__, GI->title ); + Con_Reportf( "%s( %s, 0x%x, %s )\n", __func__, GI->title, flags, language ); FS_ClearSearchPath(); - flags &= FS_MOUNT_HD|FS_MOUNT_LV|FS_MOUNT_ADDON|FS_MOUNT_L10N; + // don't let rescan set searchpath flags + flags = FBitSet( flags, FS_MOUNT_FLAGS ); - if( FBitSet( flags, FS_MOUNT_L10N )) + if( FBitSet( flags, FS_MOUNT_L10N ) && !COM_StringEmpty( language ) && Q_isalpha( language )) + { Q_strncpy( fs_language, language, sizeof( fs_language )); + } else + { fs_language[0] = 0; + ClearBits( flags, FS_MOUNT_L10N ); + } - str = getenv( "XASH3D_EXTRAS_PAK1" ); + const char *str = getenv( "XASH3D_EXTRAS_PAK1" ); if( !COM_StringEmptyOrNULL( str )) - FS_MountArchive_Fullpath( str, FS_NOWRITE_PATH|FS_CUSTOM_PATH ); + FS_MountArchive_Fullpath( str, FS_NOWRITE_PATH | FS_CUSTOM_PATH ); str = getenv( "XASH3D_EXTRAS_PAK2" ); if( !COM_StringEmptyOrNULL( str )) - FS_MountArchive_Fullpath( str, FS_NOWRITE_PATH|FS_CUSTOM_PATH ); + FS_MountArchive_Fullpath( str, FS_NOWRITE_PATH | FS_CUSTOM_PATH ); if( Q_stricmp( GI->basedir, GI->gamefolder )) FS_AddGameHierarchy( GI->basedir, flags ); + if( Q_stricmp( GI->basedir, GI->falldir ) && Q_stricmp( GI->gamefolder, GI->falldir )) FS_AddGameHierarchy( GI->falldir, flags ); @@ -1453,8 +1496,8 @@ return true if library is crypted */ static qboolean FS_CheckForCrypt( const char *dllname ) { - file_t *f; - int key; + file_t *f; + int key; // this encryption is specific to DLLs if( Q_stricmp( COM_FileExtension( dllname ), "dll" )) @@ -1490,103 +1533,6 @@ static int FS_StripIdiotRelativePath( const char *dllname, const char *gamefolde return 0; } -/* -================== -FS_FindLibrary - -search for library, assume index is valid -================== -*/ -static qboolean FS_FindLibrary( const char *dllname, qboolean directpath, fs_dllinfo_t *dllInfo ) -{ - string fixedname; - searchpath_t *search; - int index, start = 0, len; - - // check for bad exports - if( COM_StringEmptyOrNULL( dllname )) - return false; - - FS_AllowDirectPaths( directpath ); - - // HACKHACK remove relative path to game folder - if( !Q_strnicmp( dllname, "..", 2 )) - { - // some modders put relative path to themselves??? - len = FS_StripIdiotRelativePath( dllname, GI->gamefolder ); - - if( len == 0 ) // or put relative path to Half-Life game libs - len = FS_StripIdiotRelativePath( dllname, "valve" ); - start += len; - } - - Q_strnlwr( &dllname[start], dllInfo->shortPath, sizeof( dllInfo->shortPath )); // always in lower case (why?) - COM_FixSlashes( dllInfo->shortPath ); // replace all backward slashes - COM_DefaultExtension( dllInfo->shortPath, "."OS_LIB_EXT, sizeof( dllInfo->shortPath )); // apply ext if forget - - search = FS_FindFile( dllInfo->shortPath, &index, fixedname, sizeof( fixedname ), false ); - - if( search ) - { - Q_strncpy( dllInfo->shortPath, fixedname, sizeof( dllInfo->shortPath )); - } - else if( !directpath ) - { - FS_AllowDirectPaths( false ); - - // trying check also 'bin' folder for indirect paths - search = FS_FindFile( dllname, &index, fixedname, sizeof( fixedname ), false ); - if( !search ) - return false; // unable to find - - Q_strncpy( dllInfo->shortPath, fixedname, sizeof( dllInfo->shortPath )); - } - - dllInfo->encrypted = dllInfo->custom_loader = false; // predict state - - if( search && index >= 0 ) // when library is available through VFS - { - dllInfo->encrypted = FS_CheckForCrypt( dllInfo->shortPath ); - - if( search->type == SEARCHPATH_PLAIN ) // is it on the disk? (intentionally omit pk3dir here) - { - // NOTE: gamedll might resolve it's own path using dladdr() and expects absolute path - // NOTE: the only allowed case when searchpath is set by absolute path is the RoDir - // rather than figuring out whether path is absolute, just check if it matches - if( !COM_StringEmpty( fs_rodir ) && !Q_strnicmp( search->filename, fs_rodir, Q_strlen( fs_rodir ))) - { - Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s%s", search->filename, dllInfo->shortPath ); - } - else - { - Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s/%s%s", fs_rootdir, search->filename, dllInfo->shortPath ); - } - } - else - { - Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s", dllInfo->shortPath ); - Con_Printf( "%s%s: loading libraries from archives is %s\n", -#if XASH_WIN32 && XASH_X86 // a1ba: custom loader is non-portable (I just don't want to touch it) - S_WARN, __func__, "non portable and might fail on other platforms" -#else - S_ERROR, __func__, "unsupported on this platform" -#endif - ); - - dllInfo->custom_loader = true; - } - } - else - { - // NOTE: if search is NULL let OS to find the library - Q_strncpy( dllInfo->fullPath, dllInfo->shortPath, sizeof( dllInfo->fullPath )); - } - - FS_AllowDirectPaths( false ); // always reset direct paths - - return true; -} - static poolhandle_t Mem_AllocPoolStub( const char *name, const char *filename, int fileline ) { return (poolhandle_t)0xDEADC0DE; @@ -1726,9 +1672,10 @@ qboolean FS_InitStdio( qboolean unused_set_to_true, const char *rootdir, const c if( !COM_StringEmpty( fs_rodir )) { Q_snprintf( buf, sizeof( buf ), "%s/", fs_rodir ); - FS_AddGameDirectory( buf, FS_STATIC_PATH|FS_NOWRITE_PATH ); + FS_AddGameDirectory( buf, FS_STATIC_PATH | FS_NOWRITE_PATH | FS_PathExecFlag( fs_rodir )); } - FS_AddGameDirectory( "./", FS_STATIC_PATH ); + + FS_AddGameDirectory( "./", FS_STATIC_PATH | FS_PathExecFlag( fs_rootdir )); // but scan rodir for games first if( !COM_StringEmpty( fs_rodir )) @@ -2177,7 +2124,7 @@ Return the searchpath where the file was found (or NULL) and the file index in the package if relevant ==================== */ -searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t len, qboolean gamedironly ) +static searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t len, uint32_t flags ) { searchpath_t *search; @@ -2186,7 +2133,7 @@ searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t { int pack_ind; - if( gamedironly & !FBitSet( search->flags, FS_GAMEDIRONLY_SEARCH_FLAGS )) + if( flags && !FBitSet( search->flags, flags )) continue; pack_ind = search->pfnFindFile( search, name, fixedname, len ); @@ -2245,6 +2192,103 @@ searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t return NULL; } +/* +================== +FS_FindLibrary + +search for library, assume index is valid +================== +*/ +static qboolean FS_FindLibrary( const char *dllname, qboolean directpath, fs_dllinfo_t *dllInfo ) +{ + string fixedname; + searchpath_t *search; + int index, start = 0, len; + + // check for bad exports + if( COM_StringEmptyOrNULL( dllname )) + return false; + + FS_AllowDirectPaths( directpath ); + + // HACKHACK remove relative path to game folder + if( !Q_strnicmp( dllname, "..", 2 )) + { + // some modders put relative path to themselves??? + len = FS_StripIdiotRelativePath( dllname, GI->gamefolder ); + + if( len == 0 ) // or put relative path to Half-Life game libs + len = FS_StripIdiotRelativePath( dllname, "valve" ); + start += len; + } + + Q_strnlwr( &dllname[start], dllInfo->shortPath, sizeof( dllInfo->shortPath )); // always in lower case (why?) + COM_FixSlashes( dllInfo->shortPath ); // replace all backward slashes + COM_DefaultExtension( dllInfo->shortPath, "."OS_LIB_EXT, sizeof( dllInfo->shortPath )); // apply ext if forget + + search = FS_FindFile( dllInfo->shortPath, &index, fixedname, sizeof( fixedname ), FS_EXEC_PATH ); + + if( search ) + { + Q_strncpy( dllInfo->shortPath, fixedname, sizeof( dllInfo->shortPath )); + } + else if( !directpath ) + { + FS_AllowDirectPaths( false ); + + // trying check also 'bin' folder for indirect paths + search = FS_FindFile( dllname, &index, fixedname, sizeof( fixedname ), FS_EXEC_PATH ); + if( !search ) + return false; // unable to find + + Q_strncpy( dllInfo->shortPath, fixedname, sizeof( dllInfo->shortPath )); + } + + dllInfo->encrypted = dllInfo->custom_loader = false; // predict state + + if( search && index >= 0 ) // when library is available through VFS + { + dllInfo->encrypted = FS_CheckForCrypt( dllInfo->shortPath ); + + if( search->type == SEARCHPATH_PLAIN ) // is it on the disk? (intentionally omit pk3dir here) + { + // NOTE: gamedll might resolve it's own path using dladdr() and expects absolute path + // NOTE: the only allowed case when searchpath is set by absolute path is the RoDir + // rather than figuring out whether path is absolute, just check if it matches + if( !COM_StringEmpty( fs_rodir ) && !Q_strnicmp( search->filename, fs_rodir, Q_strlen( fs_rodir ))) + { + Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s%s", search->filename, dllInfo->shortPath ); + } + else + { + Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s/%s%s", fs_rootdir, search->filename, dllInfo->shortPath ); + } + } + else + { + Q_snprintf( dllInfo->fullPath, sizeof( dllInfo->fullPath ), "%s", dllInfo->shortPath ); + Con_Printf( "%s%s: loading libraries from archives is %s\n", +#if XASH_WIN32 && XASH_X86 // a1ba: custom loader is non-portable (I just don't want to touch it) + S_WARN, __func__, "non portable and might fail on other platforms" +#else + S_ERROR, __func__, "unsupported on this platform" +#endif + ); + + dllInfo->custom_loader = true; + } + } + else + { + // NOTE: if search is NULL let OS to find the library + Q_strncpy( dllInfo->fullPath, dllInfo->shortPath, sizeof( dllInfo->fullPath )); + } + + FS_AllowDirectPaths( false ); // always reset direct paths + + return true; +} + /* =========================== FS_FullPathToRelativePath @@ -2286,7 +2330,7 @@ file_t *FS_OpenReadFile( const char *filename, const char *mode, qboolean gamedi char netpath[MAX_SYSPATH]; int pack_ind; - search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly ); + search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 ); // not found? if( search == NULL ) @@ -2938,7 +2982,7 @@ static byte *FS_LoadFile_( const char *path, fs_offset_t *filesizeptr, const qbo if( !fs_searchpaths || FS_CheckNastyPath( path )) return NULL; - search = FS_FindFile( path, &pack_ind, netpath, sizeof( netpath ), gamedironly ); + search = FS_FindFile( path, &pack_ind, netpath, sizeof( netpath ), gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 ); if( !search ) return NULL; @@ -3086,7 +3130,7 @@ Look for a file in the packages and in the filesystem */ int GAME_EXPORT FS_FileExists( const char *filename, int gamedironly ) { - return FS_FindFile( filename, NULL, NULL, 0, gamedironly ) != NULL; + return FS_FindFile( filename, NULL, NULL, 0, gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 ) != NULL; } /* @@ -3120,7 +3164,7 @@ qboolean FS_GetFullDiskPath( char *buffer, size_t size, const char *name, qboole searchpath_t *search; char temp[MAX_SYSPATH]; - search = FS_FindFile( name, NULL, temp, sizeof( temp ), gamedironly ); + search = FS_FindFile( name, NULL, temp, sizeof( temp ), gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 ); if( search && search->type == SEARCHPATH_PLAIN ) { @@ -3182,8 +3226,9 @@ int FS_FileTime( const char *filename, qboolean gamedironly ) char netpath[MAX_SYSPATH]; int pack_ind; - search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly ); - if( !search ) return -1; // doesn't exist + search = FS_FindFile( filename, &pack_ind, netpath, sizeof( netpath ), gamedironly ? FS_GAMEDIRONLY_SEARCH_FLAGS : 0 ); + if( !search ) + return -1; // doesn't exist return search->pfnFileTime( search, netpath ); } diff --git a/filesystem/filesystem.h b/filesystem/filesystem.h index ff1b91ff..02251b7b 100644 --- a/filesystem/filesystem.h +++ b/filesystem/filesystem.h @@ -31,8 +31,8 @@ extern "C" { #endif // __cplusplus -#define FS_API_VERSION 4 // not stable yet! -#define FS_API_CREATEINTERFACE_TAG "XashFileSystem004" // follow FS_API_VERSION!!! +#define FS_API_VERSION 5 // not stable yet! +#define FS_API_CREATEINTERFACE_TAG "XashFileSystem005" // follow FS_API_VERSION!!! #define FILESYSTEM_INTERFACE_VERSION "VFileSystem009" // never change this! // search path flags @@ -43,14 +43,17 @@ enum FS_GAMEDIR_PATH = BIT( 2 ), // just a marker for gamedir path FS_CUSTOM_PATH = BIT( 3 ), // gamedir but with custom/mod data FS_GAMERODIR_PATH = BIT( 4 ), // gamedir but read-only + FS_EXEC_PATH = BIT( 5 ), // this directory is allowed to have executable code - FS_SKIP_ARCHIVED_WADS = BIT( 5 ), // don't mount wads inside archives automatically - FS_LOAD_PACKED_WAD = BIT( 6 ), // this wad is packed inside other archive + FS_SKIP_ARCHIVED_WADS = BIT( 16 ), // don't mount wads inside archives automatically + FS_LOAD_PACKED_WAD = BIT( 17 ), // this wad is packed inside other archive - FS_MOUNT_HD = BIT( 7 ), // mount high definition content folder - FS_MOUNT_LV = BIT( 8 ), // mount low violence content folder - FS_MOUNT_ADDON = BIT( 9 ), // mount addon folder - FS_MOUNT_L10N = BIT( 10 ), // mount localization folder + FS_MOUNT_HD = BIT( 24 ), // mount high definition content folder + FS_MOUNT_LV = BIT( 25 ), // mount low violence content folder + FS_MOUNT_ADDON = BIT( 26 ), // mount addon folder + FS_MOUNT_L10N = BIT( 27 ), // mount localization folder + + FS_MOUNT_FLAGS = FS_MOUNT_HD | FS_MOUNT_LV | FS_MOUNT_ADDON | FS_MOUNT_L10N, FS_GAMEDIRONLY_SEARCH_FLAGS = FS_GAMEDIR_PATH | FS_CUSTOM_PATH | FS_GAMERODIR_PATH }; diff --git a/filesystem/filesystem_internal.h b/filesystem/filesystem_internal.h index fec87003..c2e963ad 100644 --- a/filesystem/filesystem_internal.h +++ b/filesystem/filesystem_internal.h @@ -214,15 +214,14 @@ qboolean FS_Delete( const char *path ); qboolean FS_SysFileExists( const char *path ); const char *FS_GetDiskPath( const char *name, qboolean gamedironly ); qboolean FS_GetFullDiskPath( char *buffer, size_t size, const char *name, qboolean gamedironly ); -void FS_CreatePath( char *path ); +void FS_CreatePath( char *path ); qboolean FS_SysFolderExists( const char *path ); qboolean FS_SysFileOrFolderExists( const char *path ); file_t *FS_OpenReadFile( const char *filename, const char *mode, qboolean gamedironly ); -int FS_SysFileTime( const char *filename ); -file_t *FS_OpenHandle( searchpath_t *search, int handle, fs_offset_t offset, fs_offset_t len ); -file_t *FS_SysOpen( const char *filepath, const char *mode ); -searchpath_t *FS_FindFile( const char *name, int *index, char *fixedname, size_t len, qboolean gamedironly ); +int FS_SysFileTime( const char *filename ); +file_t *FS_OpenHandle( searchpath_t *search, int handle, fs_offset_t offset, fs_offset_t len ); +file_t *FS_SysOpen( const char *filepath, const char *mode ); qboolean FS_FullPathToRelativePath( char *dst, const char *src, size_t size ); //