/* Copyright (C) 1997-2001 Id Software, Inc. 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 "qcommon.h" #include "zlib.h" /* ============================================================================= QUAKE FILESYSTEM ============================================================================= */ #define FS_ZIP_BUFSIZE 0x00000800 #define FS_ZIP_BUFREADCOMMENT 0x00000400 #define FS_ZIP_SIZELOCALHEADER 0x0000001e #define FS_ZIP_SIZECENTRALDIRITEM 0x0000002e #define FS_ZIP_LOCALHEADERMAGIC 0x04034b50 #define FS_ZIP_CENTRALHEADERMAGIC 0x02014b50 #define FS_ZIP_ENDHEADERMAGIC 0x06054b50 typedef struct { unsigned char readBuffer[FS_ZIP_BUFSIZE]; // internal buffer for compressed data z_stream zstream; // zLib stream structure for inflate unsigned compressedSize; unsigned restReadCompressed; // number of bytes to be decompressed } zipEntry_t; typedef struct { char *name; char *searchName; } searchfile_t; #define FS_PACKFILE_DEFLATED 1 #define FS_PACKFILE_COHERENT 2 typedef struct packfile_s { char *name; unsigned flags; unsigned compressedSize; // compressed size unsigned uncompressedSize; // uncompressed size unsigned offset; // relative offset of local header struct packfile_s *hashNext; } packfile_t; // // in memory // typedef struct { char filename[MAX_OSPATH]; int numFiles; int hashSize; packfile_t *files; char *fileNames; packfile_t **filesHash; } pack_t; typedef struct filehandle_s { FILE *fstream; unsigned offset; unsigned uncompressedSize; // uncompressed size unsigned restReadUncompressed; // number of bytes to be obtained after decompession zipEntry_t *zipEntry; struct filehandle_s *prev, *next; } filehandle_t; typedef struct searchpath_s { char filename[MAX_OSPATH]; pack_t *pack; // only one of filename / pack will be used struct searchpath_s *next; } searchpath_t; cvar_t *fs_basepath; cvar_t *fs_basedir; cvar_t *fs_cdpath; cvar_t *fs_gamedirvar; static cvar_t *fs_usehomedir; static searchpath_t *fs_searchpaths; static searchpath_t *fs_base_searchpaths; // without gamedirs static mempool_t *fs_mempool; #define FS_MAX_BLOCK_SIZE 0x10000 #define FS_MAX_HASH_SIZE 1024 #define FS_MAX_HANDLES 1024 static filehandle_t fs_filehandles[FS_MAX_HANDLES]; static filehandle_t fs_filehandles_headnode, *fs_free_filehandles; // we mostly read from one file at a time, so keep a global copy of one // zipEntry to save on malloc calls and linking in FS_FOpenFile static zipEntry_t fs_globalZipEntry; static filehandle_t *fs_globalZipEntryUser; /* 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 precacution against having a malicious server instruct clients to write files over areas they shouldn't. */ /* =========== FS_PackHashKey =========== */ static unsigned FS_PackHashKey( const char *str, int hashSize ) { int c; unsigned hashval = 0; while ( (c = *str++) != 0 ) { if( c == '\\' ) c = '/'; hashval = hashval*37 + tolower( c ); } return hashval & (hashSize - 1); } /* =========== FS_PakNameForFile =========== */ char *FS_PakNameForFile( const char *filename ) { searchpath_t *search; unsigned hashKey; if( !filename || !filename[0] ) return NULL; if( strstr( filename, ".." ) || *filename == '.' // leading dot is no good || strstr( filename, "//" ) || strstr( filename, "\\\\" ) ) return NULL; if( *filename == '/' || *filename == '\\' ) filename++; // search through the path, one element at a time for( search = fs_searchpaths; search; search = search->next ) { if( search->pack ) { // is the element a pak file? pack_t *pak = search->pack; packfile_t *pakFile; hashKey = FS_PackHashKey( filename, pak->hashSize ); // look through all the pak file elements for( pakFile = pak->filesHash[hashKey]; pakFile; pakFile = pakFile->hashNext ) { if( !Q_stricmp( filename, pakFile->name ) ) { return pak->filename; } } } } return NULL; } /* =========== Cmd_PakFile_f =========== */ void Cmd_PakFile_f( void ) { char *s; s = FS_PakNameForFile( Cmd_Argv(1) ); if( !s ) Com_Printf( "Pakfile: File not found inside any pak file\n" ); else Com_Printf( "Pakfile: %s\n", s ); } /* =========== FS_OpenFileHandle =========== */ int FS_OpenFileHandle( void ) { filehandle_t *fh; if ( !fs_free_filehandles ) Sys_Error( "FS_OpenFileHandle: no free file handles" ); fh = fs_free_filehandles; fs_free_filehandles = fh->next; // put the handle at the start of the list fh->prev = &fs_filehandles_headnode; fh->next = fs_filehandles_headnode.next; fh->next->prev = fh; fh->prev->next = fh; return (fh - fs_filehandles) + 1; } /* =========== FS_FileHandleForNum =========== */ filehandle_t *FS_FileHandleForNum( int file ) { if( file < 1 || file > FS_MAX_HANDLES ) Sys_Error( "FS_FileHandleForNum: bad handle" ); if( !fs_filehandles[--file].fstream ) Sys_Error( "FS_FileHandleForNum: bad handle" ); return &fs_filehandles[file]; } /* ============== FS_CloseFileHandle ============== */ void FS_CloseFileHandle( filehandle_t *fh ) { // remove from linked open list fh->prev->next = fh->next; fh->next->prev = fh->prev; // insert into linked free list fh->next = fs_free_filehandles; fs_free_filehandles = fh; } /* ============== FS_FileExists ============== */ static int FS_FileExists( const char *path ) { searchpath_t *search; unsigned hashKey; // search through the path, one element at a time for( search = fs_searchpaths; search; search = search->next ) { if( search->pack ) { // is the element a pak file? pack_t *pak = search->pack; packfile_t *pakFile; hashKey = FS_PackHashKey( path, pak->hashSize ); // look through all the pak file elements for( pakFile = pak->filesHash[hashKey]; pakFile; pakFile = pakFile->hashNext ) { if( !Q_stricmp( path, pakFile->name ) ) return pakFile->uncompressedSize; // found it! } } else { // check a file in the directory tree FILE *f; int pos, end; char netpath[MAX_OSPATH]; Q_snprintfz( netpath, sizeof(netpath), "%s/%s", search->filename, path ); f = fopen( netpath, "rb" ); if( !f ) continue; pos = ftell( f ); fseek( f, 0, SEEK_END ); end = ftell( f ); fseek( f, pos, SEEK_SET ); fclose( f ); return end; } } return -1; } /* =========== FS_FOpenFile Finds the file in the search path. Returns filesize and an open handle Used for streaming data out of either a pak file or a separate file. =========== */ int file_from_pak; static unsigned FS_PK3CheckFileCoherency( FILE *f, packfile_t *file ); int FS_FOpenFile( const char *filename, int *filenum, int mode ) { FILE *f; searchpath_t *search; filehandle_t *file; unsigned hashKey; char netpath[MAX_OSPATH]; if( filenum ) *filenum = 0; if( !filename || !filename[0] ) return -1; if( strstr( filename, ".." ) || *filename == '.' // leading dot is no good || strstr( filename, "//" ) || strstr( filename, "\\\\" ) ) return -1; if( *filename == '/' || *filename == '\\' ) filename++; if( !filenum ) { if( mode == FS_READ ) return FS_FileExists( filename ); return -1; } if( mode == FS_WRITE ) { Q_snprintfz( netpath, sizeof(netpath), "%s/%s", FS_Gamedir(), filename ); FS_CreatePath( netpath ); f = fopen( netpath, "wb" ); if( !f ) return -1; *filenum = FS_OpenFileHandle (); file = &fs_filehandles[*filenum - 1]; file->fstream = f; file->offset = 0; file->zipEntry = NULL; file->uncompressedSize = 0; file->restReadUncompressed = 0; return 0; } else if( mode == FS_APPEND ) { int pos, end; Q_snprintfz( netpath, sizeof(netpath), "%s/%s", FS_Gamedir(), filename ); FS_CreatePath( netpath ); f = fopen( netpath, "ab" ); if( !f ) return -1; pos = ftell( f ); fseek( f, 0, SEEK_END ); end = ftell( f ); fseek( f, pos, SEEK_SET ); *filenum = FS_OpenFileHandle (); file = &fs_filehandles[*filenum - 1]; file->fstream = f; file->zipEntry = NULL; file->offset = 0; file->uncompressedSize = end; file->restReadUncompressed = 0; return end; } // search through the path, one element at a time for( search = fs_searchpaths; search; search = search->next ) { if( search->pack ) { // is the element a pak file? pack_t *pak = search->pack; packfile_t *pakFile; hashKey = FS_PackHashKey( filename, pak->hashSize ); for( pakFile = pak->filesHash[hashKey]; pakFile; pakFile = pakFile->hashNext ) { // look through all the pak file elements if( !Q_stricmp( filename, pakFile->name ) ) { // found it! *filenum = FS_OpenFileHandle (); file = &fs_filehandles[*filenum - 1]; file->fstream = fopen( pak->filename, "rb" ); file->uncompressedSize = pakFile->uncompressedSize; file->restReadUncompressed = pakFile->uncompressedSize; file->zipEntry = NULL; if( !(pakFile->flags & FS_PACKFILE_COHERENT) ) { unsigned offset = FS_PK3CheckFileCoherency( file->fstream, pakFile ); if( !offset ) { Com_DPrintf( "FS_FOpenFile: can't get proper offset for %s\n", filename ); FS_FCloseFile( *filenum ); *filenum = 0; return -1; } pakFile->offset += offset; pakFile->flags |= FS_PACKFILE_COHERENT; } file->offset = pakFile->offset; if( pakFile->flags & FS_PACKFILE_DEFLATED ) { if( fs_globalZipEntryUser ) file->zipEntry = Mem_Alloc( fs_mempool, sizeof(zipEntry_t) ); else { // reset zstream here too, Mem_Alloc does it for us above memset( &fs_globalZipEntry.zstream, 0, sizeof( fs_globalZipEntry.zstream ) ); file->zipEntry = &fs_globalZipEntry; fs_globalZipEntryUser = file; } file->zipEntry->compressedSize = pakFile->compressedSize; file->zipEntry->restReadCompressed = pakFile->compressedSize; // windowBits is passed < 0 to tell that there is no zlib header. // Note that in this case inflate *requires* an extra "dummy" byte // after the compressed stream in order to complete decompression and // return Z_STREAM_END. We don't want absolutely Z_STREAM_END because we known the // size of both compressed and uncompressed data if( inflateInit2( &file->zipEntry->zstream, -MAX_WBITS ) != Z_OK ) { Com_DPrintf( "FS_FOpenFile: can't inflate %s\n", filename ); FS_FCloseFile( *filenum ); *filenum = 0; return -1; } } if( fseek( file->fstream, file->offset, SEEK_SET ) != 0 ) { Com_DPrintf( "FS_FOpenFile: can't inflate %s\n", filename ); FS_FCloseFile( *filenum ); *filenum = 0; return -1; } file_from_pak = 1; Com_DPrintf( "PackFile: %s : %s\n", pak->filename, filename ); return pakFile->uncompressedSize; } } } else { // check a file in the directory tree int pos, end; Q_snprintfz( netpath, sizeof(netpath), "%s/%s", search->filename, filename ); f = fopen( netpath, "rb" ); if( !f ) continue; pos = ftell( f ); fseek( f, 0, SEEK_END ); end = ftell( f ); fseek( f, pos, SEEK_SET ); *filenum = FS_OpenFileHandle (); file = &fs_filehandles[*filenum - 1]; file->offset = 0; file->zipEntry = NULL; file->fstream = f; file->uncompressedSize = end; file->restReadUncompressed = end; file_from_pak = 0; Com_DPrintf( "FS_FOpenFile: %s\n", netpath ); return end; } } *filenum = 0; file_from_pak = 0; Com_DPrintf( "FS_FOpenFile: can't find %s\n", filename ); return -1; } /* ============== FS_FCloseFile ============== */ void FS_FCloseFile( int file ) { filehandle_t *fh; if( !file ) return; // return silently fh = FS_FileHandleForNum( file ); if( fh->zipEntry ) { inflateEnd( &fh->zipEntry->zstream ); if( fh != fs_globalZipEntryUser ) Mem_Free( fh->zipEntry ); else fs_globalZipEntryUser = NULL; fh->zipEntry = NULL; } if( fh->fstream ) { fclose( fh->fstream ); fh->fstream = NULL; } FS_CloseFileHandle( fh ); } /* ================= FS_ReadPK3File Properly handles partial reads, used by FS_Read and FS_Seek ================= */ static int FS_ReadPK3File( qbyte *buf, size_t len, filehandle_t *fh ) { zipEntry_t *zipEntry; size_t block, total; int read, error, totalOutBefore; zipEntry = fh->zipEntry; zipEntry->zstream.next_out = buf; zipEntry->zstream.avail_out = (unsigned int)len; total = 0; do { // read in chunks if( !zipEntry->zstream.avail_in && zipEntry->restReadCompressed ) { block = min( zipEntry->restReadCompressed, FS_ZIP_BUFSIZE ); read = (int)fread( zipEntry->readBuffer, 1, block, fh->fstream ); if( read == 0 ) { // we might have been trying to read from a CD read = (int)fread( zipEntry->readBuffer, 1, block, fh->fstream ); if( read == 0 ) read = -1; } if( read == -1 ) Sys_Error( "FS_Read: can't read %i bytes", block ); zipEntry->restReadCompressed -= (unsigned)block; zipEntry->zstream.next_in = zipEntry->readBuffer; zipEntry->zstream.avail_in = (unsigned)block; } totalOutBefore = zipEntry->zstream.total_out; error = inflate( &zipEntry->zstream, Z_SYNC_FLUSH ); total += (zipEntry->zstream.total_out - totalOutBefore); if( error == Z_STREAM_END ) break; if( error != Z_OK ) Sys_Error( "FS_ReadPK3File: can't inflate file" ); } while( zipEntry->zstream.avail_out > 0 ); return (int)total; } /* ================= FS_ReadFile Properly handles partial reads ================= */ static int FS_ReadFile( qbyte *buf, size_t len, filehandle_t *fh ) { size_t block, total; int read; total = 0; do { // read in chunks block = min( len, FS_MAX_BLOCK_SIZE ); read = (int)fread( buf, 1, block, fh->fstream ); if( read == 0 ) { // we might have been trying to read from a CD read = (int)fread( buf, 1, block, fh->fstream ); if( read == 0 ) read = -1; } if( read == -1 ) Sys_Error( "FS_Read: could not read %i bytes", block ); // do some progress bar thing here... len -= block; buf += block; total += block; } while( len > 0 ); return (int)total; } /* ================= FS_Read Properly handles partial reads ================= */ int FS_Read( void *buffer, size_t len, int file ) { filehandle_t *fh; size_t total; fh = FS_FileHandleForNum( file ); // read in chunks for progress bar if( len > fh->restReadUncompressed ) len = fh->restReadUncompressed; if( !len || !buffer ) return 0; if( fh->zipEntry ) total = FS_ReadPK3File( ( qbyte * )buffer, len, fh ); else total = FS_ReadFile( ( qbyte * )buffer, len, fh ); fh->restReadUncompressed -= (unsigned)total; return (int)total; } /* ============ FS_Printf ============ */ int FS_Printf( int file, const char *fmt, ... ) { char msg[MAX_PRINTMSG]; size_t len; va_list argptr; va_start ( argptr, fmt ); if( (len = vsprintf (msg, fmt, argptr)) > sizeof(msg) ) Sys_Error ( "FS_Printf: Buffer overflow" ); va_end ( argptr ); return FS_Write( msg, len, file ); } /* ============ FS_vPrintf wsw: Same as FS_Printf but we use a va_list because FS_Printf is not useabe in syscalls (coz of the "..." varargs) -- PLX ============ */ int FS_vPrintf( int file, const char *fmt, va_list argptr ) { char msg[MAX_PRINTMSG]; size_t len; if( (len = vsprintf (msg, fmt, argptr)) > sizeof(msg) ) Sys_Error ( "FS_Printf: Buffer overflow" ); return FS_Write( msg, len, file ); } /* ============ FS_Write Properly handles partial writes ============ */ int FS_Write( const void *buffer, size_t len, int file ) { int write; filehandle_t *fh; int block, remaining, total; qbyte *buf; fh = FS_FileHandleForNum( file ); if( fh->zipEntry ) Sys_Error( "FS_Write: writing to compressed file" ); buf = ( qbyte * )buffer; total = 0; remaining = (int)len; if( !remaining || !buf ) return 0; do { block = remaining; if( block > FS_MAX_BLOCK_SIZE ) block = FS_MAX_BLOCK_SIZE; write = (int)fwrite( buf, 1, block, fh->fstream ); if( write == 0 ) { // try once more write = (int)fwrite( buffer, 1, block, fh->fstream ); if( write == 0 ) write = -1; } if ( write == -1 ) Sys_Error( "FS_Read: can't write %i bytes", block ); remaining -= block; buf += block; total += block; } while( remaining > 0 ); fh->uncompressedSize += total; return total; } /* ============ FS_Tell ============ */ int FS_Tell( int file ) { filehandle_t *fh; fh = FS_FileHandleForNum( file ); return fh->uncompressedSize - fh->restReadUncompressed; } /* ============ FS_Tell ============ */ int FS_Seek( int file, int offset, int whence ) { filehandle_t *fh; zipEntry_t *zipEntry; int error, currentOffset; size_t remaining, block; qbyte buf[FS_ZIP_BUFSIZE * 8]; fh = FS_FileHandleForNum( file ); currentOffset = fh->uncompressedSize - fh->restReadUncompressed; if( whence == FS_SEEK_CUR ) offset += currentOffset; else if( whence == FS_SEEK_END ) offset += fh->uncompressedSize; else if( whence != FS_SEEK_SET ) return -1; // clamp so we don't get out of bounds clamp( offset, 0, (int)fh->uncompressedSize ); if( offset == currentOffset ) return 0; if( !fh->zipEntry ) { fh->restReadUncompressed = fh->uncompressedSize - offset; return fseek( fh->fstream, fh->offset + offset, SEEK_SET ); } // compressed files, doh zipEntry = fh->zipEntry; if( offset > currentOffset ) { offset -= currentOffset; } else { if( fseek( fh->fstream, fh->offset, SEEK_SET ) != 0 ) return -1; zipEntry->zstream.next_in = zipEntry->readBuffer; zipEntry->zstream.avail_in = 0; error = inflateReset( &zipEntry->zstream ); if( error != Z_OK ) Sys_Error( "FS_Seek: can't inflateReset file" ); fh->restReadUncompressed = fh->uncompressedSize; zipEntry->restReadCompressed = zipEntry->compressedSize; } remaining = offset; do { block = min( remaining, sizeof( buf ) ); FS_ReadPK3File( buf, block, fh ); remaining -= block; } while( remaining > 0 ); fh->restReadUncompressed -= offset; return 0; } /* ============ FS_Eof ============ */ int FS_Eof( int file ) { filehandle_t *fh; fh = FS_FileHandleForNum( file ); return !fh->restReadUncompressed; } /* ============ FS_FFlush ============ */ int FS_Flush( int file ) { filehandle_t *fh; fh = FS_FileHandleForNum( file ); return fflush( fh->fstream ); } /* ============ FS_LoadFile Filename are relative to the quake search path a null buffer will just return the file length without loading ============ */ int FS_LoadFile( const char *path, void **buffer, void *stack, size_t stackSize ) { qbyte *buf; unsigned int len; int fhandle; buf = NULL; // quiet compiler warning // look for it in the filesystem or pack files len = FS_FOpenFile( path, &fhandle, FS_READ ); if( !fhandle ) { if( buffer ) *buffer = NULL; return -1; } if( !buffer ) { FS_FCloseFile( fhandle ); return len; } if( stack && (stackSize > len) ) buf = stack; else buf = Mem_TempMallocExt( len + 1, 0 ); buf[len] = 0; *buffer = buf; FS_Read( buf, len, fhandle ); FS_FCloseFile( fhandle ); return len; } /* ============ FS_LoadAbsoluteFile Filename are NOT relative to the quake search path pak files are not searched a null buffer will just return the file length without loading ============ */ int FS_LoadAbsoluteFile( const char *path, void **buffer, void *stack, size_t stackSize ) { qbyte *buf; size_t len; int filenum; filehandle_t *fh; buf = NULL; // quiet compiler warning filenum = FS_OpenFileHandle(); fh = &fs_filehandles[filenum - 1]; fh->fstream = fopen( path, "rb" ); if( !fh->fstream ) { FS_CloseFileHandle( fh ); return 0; } fseek( fh->fstream, 0, SEEK_END ); len = ftell( fh->fstream ); fseek( fh->fstream, 0, SEEK_SET ); fh->offset = 0; fh->restReadUncompressed = len; if( !buffer ) { FS_FCloseFile( filenum ); return len; } if( stack && (stackSize > len) ) buf = stack; else buf = Mem_TempMallocExt( len + 1, 0 ); buf[len] = 0; *buffer = buf; if( FS_Read( buf, len, filenum ) != len ) len = 0; FS_FCloseFile( filenum ); return len; } /* ============= FS_FreeFile ============= */ void FS_FreeFile( void *buffer ) { Mem_TempFree( buffer ); } /* ================ FS_CopyFile FIXME: use FS_FOpenFile, FS_Read and FS_Write??? ================ */ void FS_CopyFile( const char *src, const char *dst ) { size_t l; FILE *f1, *f2; qbyte buffer[FS_MAX_BLOCK_SIZE]; Com_DPrintf( "FS_CopyFile (%s, %s)\n", src, dst ); f1 = fopen( src, "rb" ); if( !f1 ) return; FS_CreatePath( dst ); f2 = fopen( dst, "wb" ); if( !f2 ) { fclose( f1 ); return; } while( 1 ) { l = fread (buffer, 1, sizeof(buffer), f1); if( !l ) // we might have been trying to read from a CD l = fread (buffer, 1, sizeof(buffer), f1); if( !l ) break; fwrite( buffer, 1, l, f2 ); } fclose( f1 ); fclose( f2 ); } /* ================ FS_RemoveFile ================ */ void FS_RemoveFile( const char *name ) { remove( name ); } /* ================ FS_RenameFile ================ */ int FS_RenameFile( const char *src, const char *dst ) { return rename( src, dst ); } /* ================= FS_PK3SearchCentralDir Locate the central directory of a zipfile (at the end, just before the global comment) ================= */ inline unsigned int LittleLongRaw( const qbyte *raw ) { return (raw[3] << 24) | (raw[2] << 16) | (raw[1] << 8) | raw[0]; } inline unsigned short LittleShortRaw( const qbyte *raw ) { return (raw[1] << 8) | raw[0]; } static unsigned FS_PK3SearchCentralDir( FILE *fin ) { unsigned fileSize, backRead; unsigned maxBack = 0xffff; // maximum size of global comment unsigned char buf[FS_ZIP_BUFREADCOMMENT+4]; if( fseek( fin, 0, SEEK_END ) != 0 ) return 0; fileSize = ftell( fin ); if( maxBack > fileSize ) maxBack = fileSize; backRead = 4; while( backRead < maxBack ) { unsigned i, readSize, readPos; if( backRead + FS_ZIP_BUFREADCOMMENT > maxBack ) backRead = maxBack; else backRead += FS_ZIP_BUFREADCOMMENT; readPos = fileSize - backRead; readSize = min( FS_ZIP_BUFREADCOMMENT + 4, backRead ); if( readSize < 4 ) continue; if( fseek( fin, readPos, SEEK_SET ) != 0 ) break; if( fread( buf, 1, readSize, fin ) != readSize ) break; for( i = readSize - 3; i--; ) { // check the magic if( LittleLongRaw( buf + i ) == FS_ZIP_ENDHEADERMAGIC ) return readPos + i; } } return 0; } /* ================= FS_PK3CheckFileCoherency Read the local header of the current zipfile Check the coherency of the local header and info in the end of central directory about this file ================= */ static unsigned FS_PK3CheckFileCoherency( FILE *f, packfile_t *file ) { unsigned flags; unsigned char localHeader[31], compressed; if( fseek( f, file->offset, SEEK_SET ) != 0 ) return 0; if( fread( localHeader, 1, sizeof( localHeader ), f ) != sizeof( localHeader ) ) return 0; // check the magic if( LittleLongRaw( &localHeader[0] ) != FS_ZIP_LOCALHEADERMAGIC ) return 0; compressed = LittleShortRaw( &localHeader[8] ); if( ( compressed == Z_DEFLATED ) && !( file->flags & FS_PACKFILE_DEFLATED ) ) return 0; else if( !compressed && ( file->flags & FS_PACKFILE_DEFLATED ) ) return 0; flags = LittleShortRaw( &localHeader[6] ) & 8; if( ( LittleLongRaw( &localHeader[18] ) != file->compressedSize ) && !flags ) return 0; if( ( LittleLongRaw( &localHeader[22] ) != file->uncompressedSize ) && !flags ) return 0; return FS_ZIP_SIZELOCALHEADER + LittleShortRaw( &localHeader[26] ) + ( unsigned )LittleShortRaw( &localHeader[28] ); } /* ================= FS_PK3GetFileInfo Get Info about the current file in the zipfile, with internal only info ================= */ static unsigned FS_PK3GetFileInfo( FILE *f, unsigned pos, unsigned byteBeforeTheZipFile, packfile_t *file, size_t *fileNameLen, int *crc ) { size_t sizeRead; unsigned compressed; unsigned char infoHeader[46]; // we can't use a struct here because of packing if( fseek( f, pos, SEEK_SET ) != 0 ) return 0; if( fread( infoHeader, 1, sizeof( infoHeader ), f ) != sizeof( infoHeader ) ) return 0; // check the magic if( LittleLongRaw( &infoHeader[0] ) != FS_ZIP_CENTRALHEADERMAGIC ) return 0; compressed = LittleShortRaw( &infoHeader[10] ); if( compressed && (compressed != Z_DEFLATED) ) return 0; if( crc ) *crc = LittleLongRaw( &infoHeader[16] ); if( file ) { if( compressed == Z_DEFLATED ) file->flags |= FS_PACKFILE_DEFLATED; file->compressedSize = LittleLongRaw( &infoHeader[20] ); file->uncompressedSize = LittleLongRaw( &infoHeader[24] ); file->offset = LittleLongRaw( &infoHeader[42] ) + byteBeforeTheZipFile; } sizeRead = ( size_t )LittleShortRaw( &infoHeader[28] ); if( !sizeRead ) return 0; else if( sizeRead > MAX_QPATH - 1 ) sizeRead = MAX_QPATH - 1; if( fileNameLen ) *fileNameLen = sizeRead; if( file ) { if( fread( file->name, 1, sizeRead, f ) != sizeRead ) return 0; *(file->name + sizeRead) = 0; } return FS_ZIP_SIZECENTRALDIRITEM + ( unsigned )LittleShortRaw( &infoHeader[28] ) + ( unsigned )LittleShortRaw( &infoHeader[30] ) + ( unsigned )LittleShortRaw( &infoHeader[32] ); } /* ================= FS_LoadPK3File Takes an explicit (not game tree related) path to a pak file. Loads the header and directory, adding the files at the beginning of the list so they override previous pack files. ================= */ pack_t *FS_LoadPK3File( const char *packfilename ) { int i, hashSize; int numFiles; size_t namesLen, len; pack_t *pack = NULL; packfile_t *file; FILE *fin; char *names; unsigned hashKey; unsigned char zipHeader[20]; // we can't use a struct here because of packing unsigned offset, centralPos, sizeCentralDir, offsetCentralDir, byteBeforeTheZipFile; fin = fopen( packfilename, "rb" ); if( fin == NULL ) goto error; centralPos = FS_PK3SearchCentralDir( fin ); if( centralPos == 0 ) goto error; if( fseek( fin, centralPos, SEEK_SET ) != 0 ) goto error; if( fread( zipHeader, 1, sizeof( zipHeader ), fin ) != sizeof( zipHeader ) ) goto error; // total number of entries in the central dir on this disk numFiles = LittleShortRaw( &zipHeader[8] ); if( !numFiles ) goto error; if( LittleShortRaw( &zipHeader[10] ) != numFiles || LittleShortRaw( &zipHeader[6] ) != 0 || LittleShortRaw( &zipHeader[4] ) != 0 ) goto error; // size of the central directory sizeCentralDir = LittleLongRaw( &zipHeader[12] ); // offset of start of central directory with respect to the starting disk number offsetCentralDir = LittleLongRaw( &zipHeader[16] ); if( centralPos < offsetCentralDir + sizeCentralDir ) goto error; byteBeforeTheZipFile = centralPos - offsetCentralDir - sizeCentralDir; for( hashSize = 1; ( hashSize <= numFiles ) && (hashSize < FS_MAX_HASH_SIZE); hashSize <<= 1 ); for( i = 0, namesLen = 0, centralPos = offsetCentralDir + byteBeforeTheZipFile; i < numFiles; i++, centralPos += offset ) { offset = FS_PK3GetFileInfo( fin, centralPos, byteBeforeTheZipFile, NULL, &len, NULL ); if( !offset ) goto error; // something wrong occured namesLen += len + 1; } namesLen += 1; // add space for a guard pack = Mem_Alloc( fs_mempool, (int)( sizeof( pack_t ) + numFiles * sizeof( packfile_t ) + namesLen + hashSize * sizeof( packfile_t * ) ) ); Q_strncpyz( pack->filename, packfilename, sizeof(pack->filename) ); pack->files = ( packfile_t * )( ( qbyte * )pack + sizeof( pack_t ) ); pack->fileNames = names = ( char * )( ( qbyte * )pack->files + numFiles * sizeof( packfile_t ) ); pack->filesHash = ( packfile_t ** )( ( qbyte * )names + namesLen ); pack->numFiles = numFiles; pack->hashSize = hashSize; // add all files to hash table for( i = 0, file = pack->files, centralPos = offsetCentralDir + byteBeforeTheZipFile; i < numFiles; i++, file++, centralPos += offset, names += len + 1 ) { file->name = names; offset = FS_PK3GetFileInfo( fin, centralPos, byteBeforeTheZipFile, file, &len, NULL ); hashKey = FS_PackHashKey( file->name, hashSize ); file->hashNext = pack->filesHash[hashKey]; pack->filesHash[hashKey] = file; } fclose( fin ); Com_Printf( "Added pk3 file %s (%i files)\n", pack->filename, pack->numFiles ); return pack; error: if( fin ) fclose( fin ); if( pack ) Mem_Free( pack ); Com_Printf( "%s is not a valid pk3 file\n", packfilename ); return NULL; } // wsw : Medar /* ================= FS_AddPakFile Loads a PAK and adds it to the tree after it has been initialized. Used for downloads ================= */ void FS_AddPakFile( const char *pakname ) { pack_t *pak; searchpath_t *search; pak = FS_LoadPK3File( pakname ); if( !pak ) return; search = Mem_Alloc( fs_mempool, sizeof( searchpath_t ) ); Q_strncpyz( search->filename, pakname, sizeof( search->filename ) ); search->pack = pak; search->next = fs_searchpaths; fs_searchpaths = search; } /* ================= FS_PathGetFileListExt ================= */ int FS_PathGetFileListExt( searchpath_t *search, const char *dir, const char *extension, searchfile_t *files, size_t size ) { int i; unsigned found; char *p, *s, *name; size_t dirlen, extlen, tokenlen; if( !size ) return 0; found = 0; dirlen = 0; extlen = 0; if( dir ) { dirlen = strlen( dir ); if( dirlen ) { if( dir[dirlen-1] == '/' ) dirlen--; } } if( extension ) extlen = strlen( extension ); if( !search->pack ) { size_t searchlen; int numfiles; char **filenames; char findname[MAX_OSPATH]; unsigned int musthave, canthave; musthave = 0; canthave = SFF_HIDDEN | SFF_SYSTEM; Q_snprintfz( findname, sizeof( findname ), "%s/", search->filename ); searchlen = strlen( findname ); if( dirlen ) { Q_strncatz( findname, dir, sizeof( findname ) ); searchlen = strlen( findname ); if( findname[searchlen-1] != '/' ) { Q_strncatz( findname, "/", sizeof( findname ) ); searchlen++; } } if( extlen ) { if( extension[0] != '/' ) { Q_strncatz( findname, "*", sizeof( findname ) ); Q_strncatz( findname, extension, sizeof( findname ) ); canthave |= SFF_SUBDIR; } else { Q_strncatz( findname, "*.*", sizeof( findname ) ); musthave |= SFF_SUBDIR; } } else { Q_strncatz( findname, "*.*", sizeof( findname ) ); } if( (filenames = FS_ListFiles( findname, &numfiles, musthave, canthave )) ) { for( i = 0; i < numfiles - 1; i++ ) { if( found < size ) { if( (musthave & SFF_SUBDIR) ) { size_t len = strlen( filenames[i] + searchlen ); if( filenames[i][searchlen+len-1] != '/' ) { files[found].name = Mem_ZoneMalloc( (int)len + 2 ); strcpy( files[found].name, filenames[i] + searchlen ); files[found].name[len] = '/'; files[found].name[len+1] = 0; } else { files[found].name = CopyString( filenames[i] + searchlen ); } } else { files[found].name = CopyString( filenames[i] + searchlen ); } files[found].searchName = search->filename; found++; } Mem_ZoneFree( filenames[i] ); } } return found; } for( s = search->pack->fileNames; *s; s += tokenlen + 1 ) { tokenlen = strlen( s ); // check directory if( dirlen ) if( tokenlen <= dirlen + 1 || s[dirlen] != '/' || Q_strnicmp( s, dir, dirlen ) ) continue; // check extension if( extlen ) if( tokenlen < extlen || Q_strnicmp( extension, s + tokenlen - extlen, extlen ) ) continue; if( dirlen ) name = s + dirlen + 1; else name = s; // ignore subdirectories p = strchr( name, '/' ); if( p ) if( *(p + 1) ) continue; files[found].name = CopyString( name ); files[found].searchName = search->filename; if( ++found == size ) return found; } return found; } /* ================ FS_AddGameDirectory Sets fs_gamedir, adds the directory to the head of the path, then loads and adds pak1.pak pak2.pak ... ================ */ void FS_AddGameDirectory( const char *dir ) { int j, numpaks; searchpath_t *search; pack_t *pak; char pakfile[MAX_OSPATH]; char **paknames; // add the directory to the search path search = Mem_Alloc( fs_mempool, sizeof( searchpath_t ) ); Q_strncpyz( search->filename, dir, sizeof( search->filename ) ); search->next = fs_searchpaths; fs_searchpaths = search; Q_snprintfz( pakfile, sizeof(pakfile), "%s/*.%s", dir, "pk3" ); if( ( paknames = FS_ListFiles( pakfile, &numpaks, 0, SFF_SUBDIR | SFF_HIDDEN | SFF_SYSTEM ) ) != 0 ) { for( j = 0; j < numpaks - 1; j++ ) { pak = FS_LoadPK3File( paknames[j] ); if( !pak ) continue; search = Mem_Alloc( fs_mempool, sizeof( searchpath_t ) ); Q_strncpyz( search->filename, paknames[j], sizeof( search->filename ) ); search->pack = pak; search->next = fs_searchpaths; fs_searchpaths = search; Mem_ZoneFree( paknames[j] ); } Mem_ZoneFree( paknames ); } } /* ================ FS_AddHomeAsGameDirectory Use ~/.warsow/dir for reading and writing icculus.org ================ */ void FS_AddHomeAsGameDirectory( const char *dir ) { int len; char gdir[MAX_OSPATH]; char *homedir; homedir = Sys_GetHomeDirectory (); if( !homedir ) return; if( !fs_usehomedir->integer ) return; len = snprintf( gdir, sizeof(gdir), "%s/.warsow/%s/", homedir, dir ); FS_CreatePath( gdir ); if( (len > 0) && (len < sizeof(gdir)) && (gdir[len-1] == '/') ) gdir[len-1] = 0; FS_AddGameDirectory( gdir ); } /* ============ FS_Gamedir Called to find where to write a file (demos, savegames, etc) We will use the searchpath that isn't a pack and hast highest priority ============ */ char *FS_Gamedir( void ) { searchpath_t *search; for( search = fs_searchpaths; search; search = search->next ) { if( search->pack == NULL ) return search->filename; } return NULL; } /* ================ FS_SetGamedir Sets the gamedir and path to a different directory. ================ */ void FS_SetGamedir( char *dir ) { searchpath_t *next; if( strstr(dir, "..") || strstr(dir, "/") || strstr(dir, "\\") || strstr(dir, ":") ) { Com_Printf( "Gamedir should be a single filename, not a path\n" ); return; } // free up any current game dir info while( fs_searchpaths != fs_base_searchpaths ) { if( fs_searchpaths->pack ) Mem_Free( fs_searchpaths->pack ); next = fs_searchpaths->next; Mem_Free( fs_searchpaths ); fs_searchpaths = next; } // flush all data, so it will be forced to reload if( dedicated && !dedicated->integer ) Cbuf_AddText( "snd_restart\nin_restart\n" ); if( !strcmp (dir, BASEDIRNAME) || (*dir == 0) ) { Cvar_FullSet( "fs_gamedir", "", CVAR_SERVERINFO|CVAR_NOSET, qtrue ); Cvar_FullSet( "fs_game", "", CVAR_LATCH|CVAR_SERVERINFO, qtrue ); } else { Cvar_FullSet( "fs_gamedir", dir, CVAR_SERVERINFO|CVAR_NOSET, qtrue ); if( fs_cdpath->string[0] ) FS_AddGameDirectory( va("%s/%s", fs_cdpath->string, dir) ); FS_AddGameDirectory( va("%s/%s", fs_basepath->string, dir) ); FS_AddHomeAsGameDirectory( dir ); } Com_Printf( "Using %s for writing\n", FS_Gamedir() ); } /* ================ FS_ListFiles ================ */ char **FS_ListFiles( char *findname, int *numfiles, unsigned musthave, unsigned canthave ) { char *s; int nfiles = 0; char **list = 0; s = Sys_FindFirst( findname, musthave, canthave ); while( s ) { if( s[strlen(s)-1] != '.' ) nfiles++; s = Sys_FindNext( musthave, canthave ); } Sys_FindClose (); if( !nfiles ) return NULL; nfiles++; // add space for a guard *numfiles = nfiles; list = Mem_ZoneMalloc( sizeof( char * ) * nfiles ); s = Sys_FindFirst( findname, musthave, canthave ); nfiles = 0; while( s ) { if( s[strlen(s)-1] != '.' ) { list[nfiles] = CopyString( s ); #ifdef _WIN32 Q_strlwr( list[nfiles] ); #endif nfiles++; } s = Sys_FindNext( musthave, canthave ); } Sys_FindClose (); return list; } #define FS_MAX_SEARCHFILES 1024 int FS_SortFiles( const searchfile_t *file1, const searchfile_t *file2 ) { return Q_stricmp( (file1)->name, (file2)->name ); } /* ================ FS_Dir_f ================ */ void FS_Dir_f( void ) { int i, allfound; char dir[MAX_OSPATH]; char extension[MAX_OSPATH]; searchpath_t *search; searchfile_t files[FS_MAX_SEARCHFILES]; qboolean truncated; extension[0] = 0; if( Cmd_Argc() == 1 ) { dir[0] = '/'; dir[1] = 0; } else { if( Cmd_Argc() > 2 ) Q_strncpyz( extension, Cmd_Argv( 2 ), sizeof( extension ) ); Q_strncpyz( dir, Cmd_Argv( 1 ), sizeof( dir ) ); if( !strcmp( dir, "./" ) || !strcmp( dir, "." ) ) Q_strncpyz( dir, "/", sizeof( dir ) ); } allfound = 0; memset( files, 0, sizeof( files ) ); truncated = qfalse; for( search = fs_searchpaths ; search ; search = search->next ) { allfound += FS_PathGetFileListExt( search, dir, extension, files + allfound, FS_MAX_SEARCHFILES - allfound ); if( allfound == FS_MAX_SEARCHFILES ) { truncated = qtrue; break; // we are done } } qsort( files, allfound, sizeof (searchfile_t ), (int (*)(const void *, const void *))FS_SortFiles ); Com_Printf( "Directory of %s\n", dir ); Com_Printf( "----\n" ); // free temp memory for( i = 0; i < allfound; i++ ) { Com_Printf( "%s/%s\n", files[i].searchName, files[i].name ); Mem_ZoneFree( files[i].name ); } if( truncated ) Com_Printf( "...\n" ); Com_Printf( "\n%i files listed\n", allfound ); } /* ================ FS_GetFileListExt ================ */ int FS_GetFileListExt( const char *dir, const char *extension, char *buf, size_t *bufsize, char *buf2, size_t *buf2size ) { int i; int found, allfound; size_t len, len2, alllen, all2len; searchpath_t *search; searchfile_t files[FS_MAX_SEARCHFILES]; if( !bufsize && !buf2size ) return 0; allfound = 0; memset( files, 0, sizeof( files ) ); for( search = fs_searchpaths ; search ; search = search->next ) { allfound += FS_PathGetFileListExt( search, dir, extension, files + allfound, FS_MAX_SEARCHFILES - allfound ); if( allfound == FS_MAX_SEARCHFILES ) break; // we are done } found = 0; if( buf && bufsize ) { alllen = 0; qsort( files, allfound, sizeof (searchfile_t ), (int (*)(const void *, const void *))FS_SortFiles ); if( buf2 && buf2size ) { all2len = 0; for( i = 0; i < allfound; alllen += len + 1, all2len += len2 + 1, found++, i++ ) { len = strlen( files[i].name ); len2 = strlen( files[i].searchName ); if( *bufsize <= len + alllen || *buf2size <= len2 + all2len ) break; // we are done strcpy( buf + alllen, files[i].name ); strcpy( buf2 + all2len, files[i].searchName ); } } else { for( i = 0; i < allfound; alllen += len + 1, found++, i++ ) { len = strlen( files[i].name ); if( *bufsize <= len + alllen ) break; // we are done strcpy( buf + alllen, files[i].name ); } } } else { if( buf2size ) for( i = 0, *bufsize = *buf2size = 0; i < allfound; *bufsize += strlen(files[i].name) + 1, *buf2size += strlen(files[i].searchName) + 1, found++, i++ ); else if( bufsize ) for( i = 0, *bufsize = 0; i < allfound; *bufsize += strlen(files[i].name) + 1, found++, i++ ); else return 0; } // free temp memory for( i = 0; i < allfound; i++ ) Mem_ZoneFree( files[i].name ); return found; } /* ================ FS_GetFileList ================ */ int FS_GetFileList( const char *dir, const char *extension, char *buf, size_t bufsize ) { if( buf ) return FS_GetFileListExt( dir, extension, buf, &bufsize, NULL, NULL ); return 0; } /* ============ FS_Path_f ============ */ void FS_Path_f(void) { searchpath_t *s; Com_Printf( "Current search path:\n" ); for( s = fs_searchpaths; s; s = s->next ) { if( s == fs_base_searchpaths ) Com_Printf( "----------\n" ); if( s->pack ) Com_Printf( "%s (%i files)\n", s->pack->filename, s->pack->numFiles ); else Com_Printf( "%s\n", s->filename ); } } /* ============ FS_CreatePath Creates any directories needed to store the given filename ============ */ void FS_CreatePath( const char *path ) { char *ofs; for( ofs = ( char * )path + 1; *ofs; ofs++ ) { if( *ofs == '/' ) { // create the directory *ofs = 0; Sys_Mkdir( path ); *ofs = '/'; } } } /* ================ FS_NextPath Allows enumerating all of the directories in the search path ================ */ char *FS_NextPath( char *prevpath ) { searchpath_t *s; char *prev; if( !prevpath ) return FS_Gamedir(); prev = NULL; for( s = fs_searchpaths; s; s = s->next ) { if( s->pack ) continue; if( prevpath == prev ) return s->filename; prev = s->filename; } return NULL; } /* ================ FS_GetGamedirectoryList ================ */ int FS_GetGamedirectoryList( char *buf, size_t bufsize ) { char **modnames; int j, nummods; size_t len, alllen; if( !buf ) return 0; alllen = 0; buf[0] = '\0'; if( ( modnames = FS_ListFiles( "./*", &nummods, SFF_SUBDIR, SFF_HIDDEN | SFF_SYSTEM ) ) != 0 ) { for( j = 0; j < nummods - 1; j++ ) { len = strlen( modnames[j] ); if( bufsize <= len + alllen ) { break; } strcpy( buf + alllen, modnames[j] ); alllen += len + 1; buf[alllen] = '\0'; Mem_ZoneFree( modnames[j] ); } Mem_ZoneFree( modnames ); } return nummods; } /* ================ FS_InitFilesystem ================ */ void FS_InitFilesystem (void) { int i; fs_mempool = Mem_AllocPool( NULL, "Filesystem" ); Cmd_AddCommand( "fs_path", FS_Path_f ); Cmd_AddCommand( "fs_dir", FS_Dir_f ); Cmd_AddCommand( "fs_pakfile", Cmd_PakFile_f ); // wsw : jal fs_globalZipEntryUser = NULL; memset( fs_filehandles, 0, sizeof(fs_filehandles) ); // // link filehandles // fs_free_filehandles = fs_filehandles; fs_filehandles_headnode.prev = &fs_filehandles_headnode; fs_filehandles_headnode.next = &fs_filehandles_headnode; for( i = 0; i < FS_MAX_HANDLES - 1; i++ ) fs_filehandles[i].next = &fs_filehandles[i+1]; // basedir // allows the game to run from outside the data tree fs_basepath = Cvar_Get( "fs_basepath", "/usr/local/lib/warsow", CVAR_NOSET ); fs_basedir = Cvar_Get( "fs_basedir", BASEDIRNAME, CVAR_NOSET ); if( Sys_GetHomeDirectory() != NULL ) fs_usehomedir = Cvar_Get( "fs_usehomedir", "1", CVAR_NOSET ); // default 0 for now, but 1 after 0.071a? // cddir // Logically concatenates the cddir after the basedir for allows the game to run from outside the data tree fs_cdpath = Cvar_Get( "fs_cdpath", "", CVAR_NOSET ); if( fs_cdpath->string[0] ) FS_AddGameDirectory( va("%s/"BASEDIRNAME, fs_cdpath->string) ); // start up with baseq3 by default FS_AddGameDirectory( va("%s/"BASEDIRNAME, fs_basepath->string) ); // then add a '.qfusion/BASEDIRNAME' directory in home directory by default FS_AddHomeAsGameDirectory( BASEDIRNAME ); // any set gamedirs will be freed up to here fs_base_searchpaths = fs_searchpaths; // check for game override fs_gamedirvar = Cvar_Get( "fs_game", BASEDIRNAME, CVAR_LATCH|CVAR_SERVERINFO ); if( fs_gamedirvar->string[0] ) FS_SetGamedir( fs_gamedirvar->string ); }