X-Git-Url: http://git.xonotic.org/?a=blobdiff_plain;f=plugins%2Fvfspk3%2Fvfs.cpp;h=b98a19ce352281d4d071b9c37c5173754351be94;hb=ad24a3cb96810d8d41c40608048fa08bda94e89f;hp=65d7a2b0de1e8395afb291682390add7d8ea57fe;hpb=cc4e44e31a89c8efed942ca26e2a341466f9a3b2;p=xonotic%2Fnetradiant.git diff --git a/plugins/vfspk3/vfs.cpp b/plugins/vfspk3/vfs.cpp index 65d7a2b0..b98a19ce 100644 --- a/plugins/vfspk3/vfs.cpp +++ b/plugins/vfspk3/vfs.cpp @@ -1,32 +1,32 @@ /* -Copyright (c) 2001, Loki software, inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -Neither the name of Loki software nor the names of its contributors may be used -to endorse or promote products derived from this software without specific prior -written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY -DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ + Copyright (c) 2001, Loki software, inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list + of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + Neither the name of Loki software nor the names of its contributors may be used + to endorse or promote products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ // // Rules: @@ -41,813 +41,915 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // Leonardo Zide (leo@lokigames.com) // -#include +#include "vfs.h" + #include +#include +#include -#if defined (__linux__) || defined (__APPLE__) - #include - #include -#else - #include - #include - #define R_OK 04 - #define S_ISDIR(mode) (mode & _S_IFDIR) -#endif +#include "qerplugin.h" +#include "idatastream.h" +#include "iarchive.h" +ArchiveModules& FileSystemQ3API_getArchiveModules(); +#include "ifilesystem.h" -// TTimo: String functions -// see http://www.qeradiant.com/faq/index.cgi?file=175 -#include "str.h" +#include "generic/callback.h" +#include "string/string.h" +#include "stream/stringstream.h" +#include "os/path.h" +#include "moduleobservers.h" +#include "filematch.h" +#include "dpkdeps.h" -#include -#include -#include "vfspk3.h" -#include "vfs.h" -#include "unzip-vfspk3.h" +#define VFS_MAXDIRS 64 + +#if defined( WIN32 ) +#define PATH_MAX 260 +#endif + +#define gamemode_get GlobalRadiant().getGameMode + -typedef struct -{ - char* name; - unz_s zipinfo; - unzFile zipfile; - guint32 size; -} VFS_PAKFILE; // ============================================================================= // Global variables -static GSList* g_unzFiles; -static GSList* g_pakFiles; -static char g_strDirs[VFS_MAXDIRS][PATH_MAX]; -static int g_numDirs; -static bool g_bUsePak = true; +Archive* OpenArchive( const char* name ); + +struct archive_entry_t +{ + CopiedString name; + Archive* archive; + bool is_pakfile; +}; + +#include +#include + +typedef std::list archives_t; + +static archives_t g_archives; +static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1]; +static int g_numDirs; +static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1]; +static int g_numForbiddenDirs = 0; +static bool g_bUsePak = true; + +ModuleObservers g_observers; // ============================================================================= // Static functions -static void vfsAddSlash (char *str) -{ - int n = strlen (str); - if (n > 0) - { - if (str[n-1] != '\\' && str[n-1] != '/') - strcat (str, "/"); - } +static void AddSlash( char *str ){ + std::size_t n = strlen( str ); + if ( n > 0 ) { + if ( str[n - 1] != '\\' && str[n - 1] != '/' ) { + globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n"; + strcat( str, "/" ); + } + } } -static void vfsFixDOSName (char *src) -{ - if (src == NULL) - return; +static void FixDOSName( char *src ){ + if ( src == 0 || strchr( src, '\\' ) == 0 ) { + return; + } + + globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n"; + + while ( *src ) + { + if ( *src == '\\' ) { + *src = '/'; + } + src++; + } +} - while (*src) - { - if (*src == '\\') - *src = '/'; - src++; - } +const _QERArchiveTable* GetArchiveTable( ArchiveModules& archiveModules, const char* ext ){ + StringOutputStream tmp( 16 ); + tmp << LowerCase( ext ); + return archiveModules.findModule( tmp.c_str() ); } -static void vfsInitPakFile (const char *filename) -{ - unz_global_info gi; - unzFile uf; - guint32 i; - int err; - - uf = unzOpen (filename); - if (uf == NULL) - { - g_FuncTable.m_pfnSysFPrintf(SYS_WRN, " failed to init pak file %s\n", filename); - return; - } - g_FuncTable.m_pfnSysPrintf(" pak file: %s\n", filename); - - g_unzFiles = g_slist_append (g_unzFiles, uf); - - err = unzGetGlobalInfo (uf,&gi); - if (err != UNZ_OK) - return; - unzGoToFirstFile(uf); - - for (i = 0; i < gi.number_entry; i++) - { - char filename_inzip[NAME_MAX]; - unz_file_info file_info; - VFS_PAKFILE* file; - - err = unzGetCurrentFileInfo (uf, &file_info, filename_inzip, sizeof(filename_inzip), NULL, 0, NULL, 0); - if (err != UNZ_OK) - break; - - file = (VFS_PAKFILE*)g_malloc (sizeof (VFS_PAKFILE)); - g_pakFiles = g_slist_append (g_pakFiles, file); - - vfsFixDOSName (filename_inzip); - g_strdown (filename_inzip); - - file->name = g_strdup (filename_inzip); - file->size = file_info.uncompressed_size; - file->zipfile = uf; - memcpy (&file->zipinfo, uf, sizeof (unz_s)); - - if ((i+1) < gi.number_entry) - { - err = unzGoToNextFile(uf); - if (err!=UNZ_OK) - break; - } - } -} - -static GSList* vfsGetListInternal (const char *refdir, const char *ext, bool directories) +static Archive* InitPakFile( ArchiveModules& archiveModules, const char *filename ){ + const _QERArchiveTable* table = GetArchiveTable( archiveModules, path_get_extension( filename ) ); + + if ( table != 0 ) { + archive_entry_t entry; + entry.name = filename; + + entry.archive = table->m_pfnOpenArchive( filename ); + entry.is_pakfile = true; + g_archives.push_back( entry ); + globalOutputStream() << " pak file: " << filename << "\n"; + + return entry.archive; + } + + return 0; +} + +inline void pathlist_prepend_unique( GSList*& pathlist, char* path ){ + if ( g_slist_find_custom( pathlist, path, (GCompareFunc)path_compare ) == 0 ) { + pathlist = g_slist_prepend( pathlist, path ); + } + else + { + g_free( path ); + } +} + +class DirectoryListVisitor : public Archive::Visitor { - GSList *lst, *lst_aux, *files = NULL; - char dirname[NAME_MAX], extension[NAME_MAX], filename[NAME_MAX]; - char basedir[NAME_MAX]; - int dirlen; - char *ptr; - char *dirlist; - struct stat st; - GDir *diskdir; - int i; - - if (refdir != NULL) - { - strcpy (dirname, refdir); - g_strdown (dirname); - vfsFixDOSName (dirname); - vfsAddSlash (dirname); - } else - dirname[0] = '\0'; - dirlen = strlen (dirname); - - if (ext != NULL) - strcpy (extension, ext); - else - extension[0] = '\0'; - g_strdown (extension); - - for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst)) - { - VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data; - gboolean found = FALSE; - ptr = file->name; - - // check that the file name begins with dirname - for (i = 0; (*ptr && i < dirlen); i++, ptr++) - if (*ptr != dirname[i]) - break; - - if (i != dirlen) - continue; - - if (directories) - { - char *sep = strchr (ptr, '/'); - if (sep == NULL) - continue; - - i = sep-ptr; - - // check for duplicates - for (lst_aux = files; lst_aux; lst_aux = g_slist_next (lst_aux)) - if (strncmp ((char*)lst_aux->data, ptr, i) == 0) - { - found = TRUE; - break; - } - - if (!found) - { - char *name = g_strndup (ptr, i+1); - name[i] = '\0'; - files = g_slist_append (files, name); - } - } else - { - // check extension - char *ptr_ext = strrchr (ptr, '.'); - if ((ext != NULL) && ((ptr_ext == NULL) || (strcmp (ptr_ext+1, extension) != 0))) - continue; - - // check for duplicates - for (lst_aux = files; lst_aux; lst_aux = g_slist_next (lst_aux)) - if (strcmp ((char*)lst_aux->data, ptr) == 0) - { - found = TRUE; - break; - } - - if (!found) - files = g_slist_append (files, g_strdup (ptr)); - } - } - - for (i = 0; i < g_numDirs; i++) - { - strcpy (basedir, g_strDirs[i]); - strcat (basedir, dirname); - - diskdir = g_dir_open (basedir, 0, NULL); - - if (diskdir != NULL) - { - while (1) - { - const char* name = g_dir_read_name(diskdir); - if(name == NULL) - break; - - if (directories && (name[0] == '.')) - continue; - - sprintf (filename, "%s%s", basedir, name); - stat (filename, &st); - - if ((S_ISDIR (st.st_mode) != 0) != directories) - continue; - - gboolean found = FALSE; - - dirlist = g_strdup(name); - - g_strdown (dirlist); - - char *ptr_ext = strrchr (dirlist, '.'); - if(ext == NULL - || (ext != NULL && ptr_ext != NULL && ptr_ext[0] != '\0' && strcmp (ptr_ext+1, extension) == 0)) - { - - // check for duplicates - for (lst_aux = files; lst_aux; lst_aux = g_slist_next (lst_aux)) - if (strcmp ((char*)lst_aux->data, dirlist) == 0) - { - found = TRUE; - break; - } - - if (!found) - files = g_slist_append (files, g_strdup (dirlist)); - } - - g_free(dirlist); - } - g_dir_close (diskdir); - } - } - - return files; +GSList*& m_matches; +const char* m_directory; +public: +DirectoryListVisitor( GSList*& matches, const char* directory ) + : m_matches( matches ), m_directory( directory ) +{} +void visit( const char* name ){ + const char* subname = path_make_relative( name, m_directory ); + if ( subname != name ) { + if ( subname[0] == '/' ) { + ++subname; + } + char* dir = g_strdup( subname ); + char* last_char = dir + strlen( dir ); + if ( last_char != dir && *( --last_char ) == '/' ) { + *last_char = '\0'; + } + pathlist_prepend_unique( m_matches, dir ); + } } +}; -/*! -This behaves identically to -stricmp(a,b), except that ASCII chars -[\]^`_ come AFTER alphabet chars instead of before. This is because -it effectively converts all alphabet chars to uppercase before comparison, -while stricmp converts them to lowercase. -*/ -//!\todo Analyse the code in rtcw/q3 to see how it behaves. -static int vfsPakSort (const void *a, const void *b) +class FileListVisitor : public Archive::Visitor { - char *s1, *s2; - int c1, c2; +GSList*& m_matches; +const char* m_directory; +const char* m_extension; +public: +FileListVisitor( GSList*& matches, const char* directory, const char* extension ) + : m_matches( matches ), m_directory( directory ), m_extension( extension ) +{} +void visit( const char* name ){ + const char* subname = path_make_relative( name, m_directory ); + if ( subname != name ) { + if ( subname[0] == '/' ) { + ++subname; + } + if ( m_extension[0] == '*' || extension_equal( path_get_extension( subname ), m_extension ) ) { + pathlist_prepend_unique( m_matches, g_strdup( subname ) ); + } + } +} +}; - s1 = (char*)a; - s2 = (char*)b; +static GSList* GetListInternal( const char *refdir, const char *ext, bool directories, std::size_t depth ){ + GSList* files = 0; - do { - c1 = *s1++; - c2 = *s2++; + ASSERT_MESSAGE( refdir[strlen( refdir ) - 1] == '/', "search path does not end in '/'" ); - if (c1 >= 'a' && c1 <= 'z') + if ( directories ) { + for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i ) { - c1 -= ('a' - 'A'); + DirectoryListVisitor visitor( files, refdir ); + ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eDirectories, depth ), refdir ); } - if (c2 >= 'a' && c2 <= 'z') + } + else + { + for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i ) { - c2 -= ('a' - 'A'); + FileListVisitor visitor( files, refdir, ext ); + ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, depth ), refdir ); } + } - if ( c1 == '\\' || c1 == ':' ) - { - c1 = '/'; + files = g_slist_reverse( files ); + + return files; +} + +inline int ascii_to_upper( int c ){ + if ( c >= 'a' && c <= 'z' ) { + return c - ( 'a' - 'A' ); + } + return c; +} + +/*! + This behaves identically to stricmp(a,b), except that ASCII chars + [\]^`_ come AFTER alphabet chars instead of before. This is because + it converts all alphabet chars to uppercase before comparison, + while stricmp converts them to lowercase. + */ +static int string_compare_nocase_upper( const char* a, const char* b ){ + for (;; ) + { + int c1 = ascii_to_upper( *a++ ); + int c2 = ascii_to_upper( *b++ ); + + if ( c1 < c2 ) { + return -1; // a < b } - if ( c2 == '\\' || c2 == ':' ) - { - c2 = '/'; + if ( c1 > c2 ) { + return 1; // a > b } + if ( c1 == 0 ) { + return 0; // a == b + } + } +} - // Arnout: note - sort pakfiles in reverse order. This ensures that - // later pakfiles override earlier ones. This because the vfs module - // returns a filehandle to the first file it can find (while it should - // return the filehandle to the file in the most overriding pakfile, the - // last one in the list that is). - if (c1 < c2) - { - //return -1; // strings not equal - return 1; // strings not equal +// Arnout: note - sort pakfiles in reverse order. This ensures that +// later pakfiles override earlier ones. This because the vfs module +// returns a filehandle to the first file it can find (while it should +// return the filehandle to the file in the most overriding pakfile, the +// last one in the list that is). + +//!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files. +class PakLess +{ +public: +bool operator()( const CopiedString& self, const CopiedString& other ) const { + return string_compare_nocase_upper( self.c_str(), other.c_str() ) > 0; +} +}; + +typedef std::set Archives; + +Archive* AddPk3Dir( const char* fullpath ){ + if ( g_numDirs == VFS_MAXDIRS ) return 0; + + strncpy( g_strDirs[g_numDirs], fullpath, PATH_MAX ); + g_strDirs[g_numDirs][PATH_MAX] = '\0'; + g_numDirs++; + + { + archive_entry_t entry; + entry.name = fullpath; + entry.archive = OpenArchive( fullpath ); + entry.is_pakfile = false; + g_archives.push_back( entry ); + + return entry.archive; + } +} + +// for Daemon DPK vfs + +Archive* AddDpkDir( const char* fullpath ){ + return AddPk3Dir( fullpath ); +} + +struct pakfile_path_t +{ + CopiedString fullpath; // full pak dir or pk3dir name + bool is_pakfile; // defines is it .pk3dir or .pk3 file +}; + +typedef std::pair PakfilePathsKV; +typedef std::map PakfilePaths; // key must have no extension, only name + +static PakfilePaths g_pakfile_paths; + +void AddDpkPak( const char* name, const char* fullpath, bool is_pakfile ){ + pakfile_path_t pakfile_path; + pakfile_path.fullpath = fullpath; + pakfile_path.is_pakfile = is_pakfile; + g_pakfile_paths.insert( PakfilePathsKV( name, pakfile_path ) ); +} + +// takes name without ext, returns without ext +static const char* GetLatestDpkPakVersion( const char* name ){ + const char* maxversion = 0; + const char* result = 0; + const char* pakname; + const char* pakversion; + int namelen = string_length( name ); + + for ( PakfilePaths::iterator i = g_pakfile_paths.begin(); i != g_pakfile_paths.end(); ++i ) + { + pakname = i->first.c_str(); + if ( strncmp( pakname, name, namelen ) != 0 || pakname[namelen] != '_' ) continue; + pakversion = pakname + (namelen + 1); + if ( maxversion == 0 || DpkPakVersionCmp( pakversion, maxversion ) > 0 ){ + maxversion = pakversion; + result = pakname; } - if (c1 > c2) - { - //return 1; - return -1; + } + return result; +} + +// release string after using +static char* GetCurrentMapDpkPakName(){ + char* mapdir; + char* mapname; + int mapnamelen; + char* result = 0; + + mapname = string_clone( GlobalRadiant().getMapName() ); + mapnamelen = string_length( mapname ); + + mapdir = strrchr( mapname, '/' ); + if ( mapdir ) { + mapdir -= 12; + if ( strncmp( mapdir, ".dpkdir/maps/", 13 ) == 0 ) { + *mapdir = '\0'; + mapdir = strrchr( mapname, '/' ); + if ( mapdir ) mapdir++; + else mapdir = mapname; + result = string_clone( mapdir ); } - } while (c1); + } + + string_release( mapname, mapnamelen ); + return result; - return 0; // strings are equal } +// prevent loading duplicates or circular references +static Archives g_loaded_dpk_paks; + +// actual pak adding on initialise, deferred from InitDirectory +// Daemon DPK filesystem doesn't need load all paks it finds +static void LoadDpkPakWithDeps( const char* pakname ){ + Archive* arc; + ArchiveTextFile* depsFile; + + if (pakname == NULL) { + // load DEPS from game pack + StringOutputStream baseDirectory( 256 ); + const char* basegame = GlobalRadiant().getRequiredGameDescriptionKeyValue( "basegame" ); + baseDirectory << GlobalRadiant().getGameToolsPath() << basegame << '/'; + arc = AddDpkDir( baseDirectory.c_str() ); + depsFile = arc->openTextFile( "DEPS" ); + } else { + const char* und = strrchr( pakname, '_' ); + if ( !und ) { + pakname = GetLatestDpkPakVersion( pakname ); + } + if ( !pakname || g_loaded_dpk_paks.find( pakname ) != g_loaded_dpk_paks.end() ) { + return; + } + + PakfilePaths::iterator i = g_pakfile_paths.find( pakname ); + if ( i == g_pakfile_paths.end() ) { + return; + } + + if ( i->second.is_pakfile ){ + arc = InitPakFile( FileSystemQ3API_getArchiveModules(), i->second.fullpath.c_str() ); + } else { + arc = AddDpkDir( i->second.fullpath.c_str() ); + } + g_loaded_dpk_paks.insert( pakname ); + + depsFile = arc->openTextFile( "DEPS" ); + } + + if ( !depsFile ) { + return; + } + + { + TextLinesInputStream istream = depsFile->getInputStream(); + + CopiedString line; + char *p_name; + char *p_version; + while ( line = istream.readLine(), string_length( line.c_str() ) ) { + if ( !DpkReadDepsLine( line.c_str(), &p_name, &p_version ) ) continue; + if ( !p_version ) { + const char* p_latest = GetLatestDpkPakVersion( p_name ); + if ( p_latest ) LoadDpkPakWithDeps( p_latest ); + } else { + int len = string_length( p_name ) + string_length( p_version ) + 1; + char* p_pakname = string_new( len ); + sprintf( p_pakname, "%s_%s", p_name, p_version ); + LoadDpkPakWithDeps( p_pakname ); + string_release( p_pakname, len ); + } + string_release( p_name, string_length( p_name ) ); + if ( p_version ) string_release( p_version, string_length( p_version ) ); + } + } + + depsFile->release(); +} + +// end for Daemon DPK vfs + // ============================================================================= // Global functions // reads all pak files from a dir -/*! -The gamemode hacks in here will do undefined things with files called zz_*. -This is simple to fix by cleaning up the hacks, but may be better left alone -if the engine code does the same thing. -*/ -void vfsInitDirectory (const char *path) -{ - char filename[PATH_MAX]; - GDir *dir; - GSList *dirlist = NULL; - int iGameMode; // 0: no filtering 1: SP 2: MP +void InitDirectory( const char* directory, ArchiveModules& archiveModules ){ + int j; + + g_numForbiddenDirs = 0; + StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " ); + for ( j = 0; j < VFS_MAXDIRS; ++j ) + { + const char *t = st.getToken(); + if ( string_empty( t ) ) { + break; + } + strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX ); + g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0'; + ++g_numForbiddenDirs; + } + + for ( j = 0; j < g_numForbiddenDirs; ++j ) + { + char* dbuf = g_strdup( directory ); + if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) { + dbuf[strlen( dbuf ) - 1] = 0; + } + const char *p = strrchr( dbuf, '/' ); + p = ( p ? ( p + 1 ) : dbuf ); + if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) { + g_free( dbuf ); + break; + } + g_free( dbuf ); + } + if ( j < g_numForbiddenDirs ) { + printf( "Directory %s matched by forbidden dirs, removed\n", directory ); + return; + } + + if ( g_numDirs == VFS_MAXDIRS ) { + return; + } - if (g_numDirs == (VFS_MAXDIRS-1)) - return; + strncpy( g_strDirs[g_numDirs], directory, PATH_MAX ); + g_strDirs[g_numDirs][PATH_MAX] = '\0'; + FixDOSName( g_strDirs[g_numDirs] ); + AddSlash( g_strDirs[g_numDirs] ); - // See if we are in "sp" or "mp" mapping mode - const char* gamemode = g_FuncTable.m_pfnReadProjectKey("gamemode"); + const char* path = g_strDirs[g_numDirs]; + + g_numDirs++; - if (gamemode) { - if (strcmp (gamemode, "sp") == 0) - iGameMode = 1; - else if (strcmp (gamemode, "mp") == 0) - iGameMode = 2; - else - iGameMode = 0; - } else - iGameMode = 0; + archive_entry_t entry; + entry.name = path; + entry.archive = OpenArchive( path ); + entry.is_pakfile = false; + g_archives.push_back( entry ); + } - strcpy (g_strDirs[g_numDirs], path); - vfsFixDOSName (g_strDirs[g_numDirs]); - vfsAddSlash (g_strDirs[g_numDirs]); - g_numDirs++; + if ( g_bUsePak ) { - if (g_bUsePak) - { - dir = g_dir_open (path, 0, NULL); + GDir* dir = g_dir_open( path, 0, 0 ); - if (dir != NULL) - { - g_FuncTable.m_pfnSysPrintf("vfs directory: %s\n", path); + if ( dir != 0 ) { + globalOutputStream() << "vfs directory: " << path << "\n"; - for(;;) - { - const char* name = g_dir_read_name(dir); - if(name == NULL) - break; + Archives archives; + Archives archivesOverride; + const char* ignore_prefix = ""; + const char* override_prefix = ""; + bool is_pk3_vfs, is_pk4_vfs, is_dpk_vfs; - char *ext = (char*)strrchr(name, '.'); - if ((ext == NULL) || (strcasecmp (ext, ".pk3") != 0)) - continue; + is_pk3_vfs = GetArchiveTable( archiveModules, "pk3" ); + is_pk4_vfs = GetArchiveTable( archiveModules, "pk4" ); + is_dpk_vfs = GetArchiveTable( archiveModules, "dpk" ); - char* direntry = g_strdup(name); + if ( !is_dpk_vfs ) { + // See if we are in "sp" or "mp" mapping mode + const char* gamemode = gamemode_get(); - // using the same kludge as in engine to ensure consistency - switch (iGameMode) - { - case 1: // SP - if (strncmp(direntry,"sp_",3) == 0) - memcpy(direntry,"zz",2); - break; - case 2: // MP - if (strncmp(direntry,"mp_",3) == 0) - memcpy(direntry,"zz",2); + if ( strcmp( gamemode, "sp" ) == 0 ) { + ignore_prefix = "mp_"; + override_prefix = "sp_"; + } + else if ( strcmp( gamemode, "mp" ) == 0 ) { + ignore_prefix = "sp_"; + override_prefix = "mp_"; + } + } + + for (;; ) + { + const char* name = g_dir_read_name( dir ); + if ( name == 0 ) { break; } - dirlist = g_slist_append (dirlist, direntry); - } + for ( j = 0; j < g_numForbiddenDirs; ++j ) + { + const char *p = strrchr( name, '/' ); + p = ( p ? ( p + 1 ) : name ); + if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) { + break; + } + } + if ( j < g_numForbiddenDirs ) { + continue; + } - g_dir_close (dir); + const char *ext = strrchr( name, '.' ); + char tmppath[PATH_MAX]; + + if ( is_dpk_vfs ) { + if ( !!ext && !string_compare_nocase_upper( ext, ".dpkdir" ) ) { + snprintf( tmppath, PATH_MAX, "%s%s/", path, name ); + tmppath[PATH_MAX] = '\0'; + FixDOSName( tmppath ); + AddSlash( tmppath ); + AddDpkPak( CopiedString( StringRange( name, ext ) ).c_str(), tmppath, false ); + } + } - // sort them - dirlist = g_slist_sort (dirlist, vfsPakSort); + if ( is_pk3_vfs || is_pk4_vfs ) { + if ( !!ext && ( !string_compare_nocase_upper( ext, ".pk3dir" ) + || !string_compare_nocase_upper( ext, ".pk4dir" ) ) ) { + snprintf( tmppath, PATH_MAX, "%s%s/", path, name ); + tmppath[PATH_MAX] = '\0'; + FixDOSName( tmppath ); + AddSlash( tmppath ); + AddPk3Dir( tmppath ); + } + } - // add the entries to the vfs and free the list - while (dirlist) - { - GSList *cur = dirlist; - char* name = (char*)cur->data; + // GetArchiveTable() needs "pk3" if ext is ".pk3" + if ( ( ext == 0 ) || *( ext + 1 ) == '\0' || GetArchiveTable( archiveModules, ext + 1 ) == 0 ) { + continue; + } - switch (iGameMode) - { - case 1: // SP - if (strncmp(name,"mp_",3) == 0) - { - g_free (name); - dirlist = g_slist_remove (cur, name); - continue; - } else if (strncmp(name,"zz_",3) == 0) - memcpy(name,"sp",2); - break; - case 2: // MP - if (strncmp(name,"sp_",3) == 0) - { - g_free (name); - dirlist = g_slist_remove (cur, name); - continue; - } else if (strncmp(name,"zz_",3) == 0) - memcpy(name,"mp",2); - break; + // using the same kludge as in engine to ensure consistency + if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) { + continue; + } + if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) { + if ( !string_compare_nocase_upper( ext, ".dpk" ) ) { + if ( is_dpk_vfs ) { + archives.insert( name ); + } + } + else { + archivesOverride.insert( name ); + } + continue; } - sprintf (filename, "%s/%s", path, name); - vfsInitPakFile (filename); + archives.insert( name ); + } - g_free (name); - dirlist = g_slist_remove (cur, name); + g_dir_close( dir ); + + // add the entries to the vfs + char* fullpath; + if ( is_dpk_vfs ) { + for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i ) { + const char* name = i->c_str(); + const char* ext = strrchr( name, '.' ); + if ( !string_compare_nocase_upper( ext, ".dpk" ) ) { + CopiedString name_final = CopiedString( StringRange( name, ext ) ); + fullpath = string_new_concat( path, name ); + AddDpkPak( name_final.c_str(), fullpath, true ); + string_release( fullpath, string_length( fullpath ) ); + } + } + } + if ( is_pk3_vfs || is_pk4_vfs ) { + for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i ) + { + const char* name = i->c_str(); + const char* ext = strrchr( name, '.' ); + if ( !string_compare_nocase_upper( ext, ".pk3" ) + || !string_compare_nocase_upper( ext, ".pk4" ) ) { + fullpath = string_new_concat( path, i->c_str() ); + InitPakFile( archiveModules, fullpath ); + string_release( fullpath, string_length( fullpath ) ); + } + } + for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i ) + { + const char* name = i->c_str(); + const char* ext = strrchr( name, '.' ); + if ( !string_compare_nocase_upper( ext, ".pk3" ) + || !string_compare_nocase_upper( ext, ".pk4" ) ) { + fullpath = string_new_concat( path, i->c_str() ); + InitPakFile( archiveModules, fullpath ); + string_release( fullpath, string_length( fullpath ) ); + } + } } - } - else - g_FuncTable.m_pfnSysFPrintf(SYS_WRN, "vfs directory not found: %s\n", path); - } + } + else + { + globalErrorStream() << "vfs directory not found: " << path << "\n"; + } + } } // frees all memory that we allocated // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant? // (for instance when modifying the project settings) -void vfsShutdown () -{ - while (g_unzFiles) - { - unzClose ((unzFile)g_unzFiles->data); - g_unzFiles = g_slist_remove (g_unzFiles, g_unzFiles->data); - } - - // avoid dangling pointer operation (makes BC hangry) - GSList *cur = g_pakFiles; - GSList *next = cur; - while (next) - { - cur = next; - VFS_PAKFILE* file = (VFS_PAKFILE*)cur->data; - g_free (file->name); - g_free (file); - next = g_slist_remove (cur, file); - } - g_pakFiles = NULL; -} - -void vfsFreeFile (void *p) -{ - g_free(p); +void Shutdown(){ + for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i ) + { + ( *i ).archive->release(); + } + g_archives.clear(); + + g_numDirs = 0; + g_numForbiddenDirs = 0; + + g_pakfile_paths.clear(); + g_loaded_dpk_paks.clear(); } -GSList* vfsGetFileList (const char *dir, const char *ext) -{ - return vfsGetListInternal (dir, ext, false); +#define VFS_SEARCH_PAK 0x1 +#define VFS_SEARCH_DIR 0x2 + +int GetFileCount( const char *filename, int flag ){ + int count = 0; + char fixed[PATH_MAX + 1]; + + strncpy( fixed, filename, PATH_MAX ); + fixed[PATH_MAX] = '\0'; + FixDOSName( fixed ); + + if ( !flag ) { + flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR; + } + + for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i ) + { + if ( (( *i ).is_pakfile && ( flag & VFS_SEARCH_PAK ) != 0) + || (!( *i ).is_pakfile && ( flag & VFS_SEARCH_DIR ) != 0) ) { + if ( ( *i ).archive->containsFile( fixed ) ) { + ++count; + } + } + } + + return count; } -GSList* vfsGetDirList (const char *dir) -{ - return vfsGetListInternal (dir, NULL, true); +ArchiveFile* OpenFile( const char* filename ){ + ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" ); + for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i ) + { + ArchiveFile* file = ( *i ).archive->openFile( filename ); + if ( file != 0 ) { + return file; + } + } + + return 0; } -void vfsClearFileDirList (GSList **lst) -{ - while (*lst) - { - g_free ((*lst)->data); - *lst = g_slist_remove (*lst, (*lst)->data); - } +ArchiveTextFile* OpenTextFile( const char* filename ){ + ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" ); + for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i ) + { + ArchiveTextFile* file = ( *i ).archive->openTextFile( filename ); + if ( file != 0 ) { + return file; + } + } + + return 0; } -int vfsGetFileCount (const char *filename, int flag) -{ - int i, count = 0; - char fixed[NAME_MAX], tmp[NAME_MAX]; - GSList *lst; - - strcpy (fixed, filename); - vfsFixDOSName (fixed); - g_strdown (fixed); - - if (!flag || (flag & VFS_SEARCH_PAK)) - { - for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst)) - { - VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data; - - if (strcmp (file->name, fixed) == 0) - count++; - } - } - - if (!flag || (flag & VFS_SEARCH_DIR)) - { - for (i = 0; i < g_numDirs; i++) - { - strcpy (tmp, g_strDirs[i]); - strcat (tmp, fixed); - if (access (tmp, R_OK) == 0) - count++; - } - } - - return count; -} - -// open a full path file -int vfsLoadFullPathFile (const char *filename, void **bufferptr) -{ - FILE *f; - long len; +// NOTE: when loading a file, you have to allocate one extra byte and set it to \0 +std::size_t LoadFile( const char *filename, void **bufferptr, int index ){ + char fixed[PATH_MAX + 1]; - f = fopen (filename, "rb"); - if (f == NULL) - return -1; + strncpy( fixed, filename, PATH_MAX ); + fixed[PATH_MAX] = '\0'; + FixDOSName( fixed ); - fseek (f, 0, SEEK_END); - len = ftell (f); - rewind (f); + ArchiveFile* file = OpenFile( fixed ); - *bufferptr = g_malloc (len+1); - if (*bufferptr == NULL) - return -1; + if ( file != 0 ) { + *bufferptr = malloc( file->size() + 1 ); + // we need to end the buffer with a 0 + ( (char*) ( *bufferptr ) )[file->size()] = 0; - fread (*bufferptr, 1, len, f); - fclose (f); + std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() ); + file->release(); + return length; + } - // we need to end the buffer with a 0 - ((char*) (*bufferptr))[len] = 0; + *bufferptr = 0; + return 0; +} - return len; +void FreeFile( void *p ){ + free( p ); } -// NOTE: when loading a file, you have to allocate one extra byte and set it to \0 -int vfsLoadFile (const char *filename, void **bufferptr, int index) -{ - int i, count = 0; - char tmp[NAME_MAX], fixed[NAME_MAX]; - GSList *lst; - - *bufferptr = NULL; - strcpy (fixed, filename); - vfsFixDOSName (fixed); - g_strdown (fixed); - - for (i = 0; i < g_numDirs; i++) - { - strcpy (tmp, g_strDirs[i]); - strcat (tmp, filename); - if (access (tmp, R_OK) == 0) - { - if (count == index) - { - return vfsLoadFullPathFile(tmp,bufferptr); - } - - count++; - } - } - - for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst)) - { - VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data; - - if (strcmp (file->name, fixed) != 0) - continue; - - if (count == index) - { - memcpy (file->zipfile, &file->zipinfo, sizeof (unz_s)); - - if (unzOpenCurrentFile (file->zipfile) != UNZ_OK) - return -1; - - *bufferptr = g_malloc (file->size+1); - // we need to end the buffer with a 0 - ((char*) (*bufferptr))[file->size] = 0; - - i = unzReadCurrentFile (file->zipfile , *bufferptr, file->size); - unzCloseCurrentFile (file->zipfile); - if (i > 0) - return file->size; - else - return -1; - } - - count++; - } - - return -1; -} - -//#ifdef _DEBUG -#if 1 - #define DBG_RLTPATH -#endif +GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){ + return GetListInternal( dir, ext, false, depth ); +} -/*! -\param shorten will try to match against the short version -recent switch back to short path names in project settings has broken some stuff -with shorten == true, we will convert in to short version before looking for root -FIXME WAAA .. the stuff below is much more simple on linux .. add appropriate #ifdef -*/ -char* vfsExtractRelativePath_short(const char *in, bool shorten) -{ - int i; - char l_in[PATH_MAX]; - char check[PATH_MAX]; - static char out[PATH_MAX]; - out[0] = 0; - -#ifdef DBG_RLTPATH - Sys_Printf("vfsExtractRelativePath: %s\n", in); -#endif +GSList* GetDirList( const char *dir, std::size_t depth ){ + return GetListInternal( dir, 0, true, depth ); +} -#ifdef _WIN32 - if (shorten) - { - // make it short - if (GetShortPathName(in, l_in, PATH_MAX) == 0) - { -#ifdef DBG_RLTPATH - Sys_Printf("GetShortPathName failed\n"); -#endif - return NULL; - } - } - else - { - strcpy(l_in,in); - } - vfsCleanFileName(l_in); -#else - strcpy(l_in, in); - vfsCleanFileName(l_in); -#endif // ifdef WIN32 - - -#ifdef DBG_RLTPATH - Sys_Printf("cleaned path: %s\n", l_in); -#endif +void ClearFileDirList( GSList **lst ){ + while ( *lst ) + { + g_free( ( *lst )->data ); + *lst = g_slist_remove( *lst, ( *lst )->data ); + } +} - for (i = 0; i < g_numDirs; i++) - { - strcpy(check,g_strDirs[i]); - vfsCleanFileName(check); -#ifdef DBG_RLTPATH - Sys_Printf("Matching against %s\n", check); -#endif +const char* FindFile( const char* relative ){ + for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i ) + { + if ( ( *i ).archive->containsFile( relative ) ) { + return ( *i ).name.c_str(); + } + } - // try to find a match - if (strstr(l_in, check)) - { - strcpy(out,l_in+strlen(check)+1); - break; - } - - } - if (out[0]!=0) - { -#ifdef DBG_RLTPATH - Sys_Printf("vfsExtractRelativePath: success\n"); -#endif - return out; - } -#ifdef DBG_RLTPATH - Sys_Printf("vfsExtractRelativePath: failed\n"); -#endif - return NULL; + return ""; } +const char* FindPath( const char* absolute ){ + const char *best = ""; + for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i ) + { + if ( string_length( ( *i ).name.c_str() ) > string_length( best ) ) { + if ( path_equal_n( absolute, ( *i ).name.c_str(), string_length( ( *i ).name.c_str() ) ) ) { + best = ( *i ).name.c_str(); + } + } + } -// FIXME TTimo: this and the above should be merged at some point -char* vfsExtractRelativePath(const char *in) -{ - static char out[PATH_MAX]; - unsigned int i, count; - char *chunk, *backup = NULL; // those point to out stuff - char *ret = vfsExtractRelativePath_short(in, false); - if (!ret) - { -#ifdef DBG_RLTPATH - Sys_Printf("trying with a short version\n"); -#endif - ret = vfsExtractRelativePath_short(in, true); - if (ret) - { - // ok, but we have a relative short version now - // hack the long relative version out of here - count = 0; - for(i=0;idata; - - char *ptr,*lastptr; - lastptr = file->name; - - while ((ptr = strchr(lastptr,'/')) != NULL) - lastptr = ptr+1; - - if (strcmp (lastptr, fixed) == 0) - { - strncpy(out,file->name,PATH_MAX); - return out; - } - } - - } - - if (!flag || (flag & VFS_SEARCH_DIR)) - { - for (i = 0; i < g_numDirs; i++) - { - strcpy (tmp, g_strDirs[i]); - strcat (tmp, in); - if (access (tmp, R_OK) == 0) - { - if (count == index) - { - strcpy (out, tmp); - return out; - } - count++; - } - } - } - return NULL; -} - - -// TODO TTimo on linux the base prompt is ~/.q3a/ -// given the file dialog, we could push the strFSBasePath and ~/.q3a into the directory shortcuts -// FIXME TTimo is this really a VFS functionality? -// actually .. this should be the decision of the core isn't it? -// or .. add an API so that the base prompt can be set during VFS init -const char* vfsBasePromptPath() + return best; +} + + +class Quake3FileSystem : public VirtualFileSystem { -#ifdef _WIN32 - static const char* path = "C:"; -#else - static const char* path = "/"; -#endif - return path; +public: +void initDirectory( const char *path ){ + InitDirectory( path, FileSystemQ3API_getArchiveModules() ); +} +void initialise(){ + load(); + globalOutputStream() << "filesystem initialised\n"; + g_observers.realise(); +} + +void load(){ + ArchiveModules& archiveModules = FileSystemQ3API_getArchiveModules(); + bool is_dpk_vfs = GetArchiveTable( archiveModules, "dpk" ); + + if ( is_dpk_vfs ) { + const char* pakname; + g_loaded_dpk_paks.clear(); + + // Load DEPS from game pack + LoadDpkPakWithDeps( NULL ); + + // prevent VFS double start, for MapName="" and MapName="unnamed.map" + if ( string_length( GlobalRadiant().getMapName() ) ){ + // load map's paks from DEPS + char* mappakname = GetCurrentMapDpkPakName(); + if ( mappakname != NULL ) { + LoadDpkPakWithDeps( mappakname ); + string_release( mappakname, string_length( mappakname ) ); + } + } + + g_pakfile_paths.clear(); + g_loaded_dpk_paks.clear(); + } +} + +void clear() { + // like shutdown() but does not unrealise (keep map etc.) + Shutdown(); +} + +void refresh(){ + // like initialise() but does not realise (keep map etc.) + load(); + globalOutputStream() << "filesystem refreshed\n"; +} + +void shutdown(){ + g_observers.unrealise(); + globalOutputStream() << "filesystem shutdown\n"; + Shutdown(); +} + +int getFileCount( const char *filename, int flags ){ + return GetFileCount( filename, flags ); +} +ArchiveFile* openFile( const char* filename ){ + return OpenFile( filename ); +} +ArchiveTextFile* openTextFile( const char* filename ){ + return OpenTextFile( filename ); +} +std::size_t loadFile( const char *filename, void **buffer ){ + return LoadFile( filename, buffer, 0 ); } +void freeFile( void *p ){ + FreeFile( p ); +} + +void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){ + GSList* list = GetDirList( basedir, depth ); + + for ( GSList* i = list; i != 0; i = g_slist_next( i ) ) + { + callback( reinterpret_cast( ( *i ).data ) ); + } + ClearFileDirList( &list ); +} +void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){ + GSList* list = GetFileList( basedir, extension, depth ); + + for ( GSList* i = list; i != 0; i = g_slist_next( i ) ) + { + const char* name = reinterpret_cast( ( *i ).data ); + if ( extension_equal( path_get_extension( name ), extension ) ) { + callback( name ); + } + } + + ClearFileDirList( &list ); +} +GSList* getDirList( const char *basedir ){ + return GetDirList( basedir, 1 ); +} +GSList* getFileList( const char *basedir, const char *extension ){ + return GetFileList( basedir, extension, 1 ); +} +void clearFileDirList( GSList **lst ){ + ClearFileDirList( lst ); +} + +const char* findFile( const char *name ){ + return FindFile( name ); +} +const char* findRoot( const char *name ){ + return FindPath( name ); +} + +void attach( ModuleObserver& observer ){ + g_observers.attach( observer ); +} +void detach( ModuleObserver& observer ){ + g_observers.detach( observer ); +} + +Archive* getArchive( const char* archiveName, bool pakonly ){ + for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i ) + { + if ( pakonly && !( *i ).is_pakfile ) { + continue; + } + + if ( path_equal( ( *i ).name.c_str(), archiveName ) ) { + return ( *i ).archive; + } + } + return 0; +} +void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){ + if ( reverse ) { + g_archives.reverse(); + } + + for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i ) + { + if ( pakonly && !( *i ).is_pakfile ) { + continue; + } + + callback( ( *i ).name.c_str() ); + } + + if ( reverse ) { + g_archives.reverse(); + } +} +}; + + +Quake3FileSystem g_Quake3FileSystem; + +VirtualFileSystem& GetFileSystem(){ + return g_Quake3FileSystem; +} + +void FileSystem_Init(){ +} + +void FileSystem_Shutdown(){ +}