]> de.git.xonotic.org Git - xonotic/netradiant.git/blobdiff - plugins/vfspk3/vfs.cpp
fix invalid use of <glib/...> headers
[xonotic/netradiant.git] / plugins / vfspk3 / vfs.cpp
index 65d7a2b0de1e8395afb291682390add7d8ea57fe..893f002bdf66f46351df1b5d3325347aecac9550 100644 (file)
@@ -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,665 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 // Leonardo Zide (leo@lokigames.com)
 //
 
-#include <glib.h>
+#include "vfs.h"
+
 #include <stdio.h>
+#include <stdlib.h>
+#include <glib.h>
 
-#if defined (__linux__) || defined (__APPLE__)
-  #include <dirent.h>
-  #include <unistd.h>
-#else
-  #include <wtypes.h>
-  #include <io.h>
-  #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 <stdlib.h>
-#include <sys/stat.h>
 
-#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 <list>
+
+typedef std::list<archive_entry_t> 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++;
+       }
 }
 
-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)
+
+
+const _QERArchiveTable* GetArchiveTable( ArchiveModules& archiveModules, const char* ext ){
+       StringOutputStream tmp( 16 );
+       tmp << LowerCase( ext );
+       return archiveModules.findModule( tmp.c_str() );
+}
+static void 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";
+       }
+}
+
+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 = '/';
-               }
-               if ( c2 == '\\' || c2 == ':' )
-               {
-                       c2 = '/';
-               }
+       files = g_slist_reverse( files );
 
-               // 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
+       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 (c1 > c2)
-               {
-                       //return 1;
-                       return -1;
+               if ( c1 > c2 ) {
+                       return 1; // a > b
+               }
+               if ( c1 == 0 ) {
+                       return 0; // a == b
                }
-       } while (c1);
+       }
+}
+
+// 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).
 
-       return 0;               // strings are equal
+//!\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<CopiedString, PakLess> Archives;
 
 // =============================================================================
 // 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;
 
-  if (g_numDirs == (VFS_MAXDIRS-1))
-    return;
-
-  // See if we are in "sp" or "mp" mapping mode
-  const char* gamemode = g_FuncTable.m_pfnReadProjectKey("gamemode");
+       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;
+       }
 
-       if (gamemode)
+       for ( j = 0; j < g_numForbiddenDirs; ++j )
        {
-               if (strcmp (gamemode, "sp") == 0)
-                       iGameMode = 1;
-               else if (strcmp (gamemode, "mp") == 0)
-                       iGameMode = 2;
-               else
-                       iGameMode = 0;
-       } else
-               iGameMode = 0;
+               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;
+       }
 
-  strcpy (g_strDirs[g_numDirs], path);
-  vfsFixDOSName (g_strDirs[g_numDirs]);
-  vfsAddSlash (g_strDirs[g_numDirs]);
-  g_numDirs++;
+       if ( g_numDirs == VFS_MAXDIRS ) {
+               return;
+       }
 
-  if (g_bUsePak)
-  {
-    dir = g_dir_open (path, 0, NULL);
+       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] );
 
-    if (dir != NULL)
-    {
-      g_FuncTable.m_pfnSysPrintf("vfs directory: %s\n", path);
+       const char* path = g_strDirs[g_numDirs];
 
-      for(;;)
-      {
-        const char* name = g_dir_read_name(dir);
-        if(name == NULL)
-          break;
+       g_numDirs++;
 
-        char *ext = (char*)strrchr(name, '.');
-        if ((ext == NULL) || (strcasecmp (ext, ".pk3") != 0))
-          continue;
+       {
+               archive_entry_t entry;
+               entry.name = path;
+               entry.archive = OpenArchive( path );
+               entry.is_pakfile = false;
+               g_archives.push_back( entry );
+       }
 
-        char* direntry = g_strdup(name);
+       if ( g_bUsePak ) {
+               GDir* dir = g_dir_open( path, 0, 0 );
 
-                               // 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);
-                                       break;
-                               }
+               if ( dir != 0 ) {
+                       globalOutputStream() << "vfs directory: " << path << "\n";
 
-                               dirlist = g_slist_append (dirlist, direntry);
-      }
+                       const char* ignore_prefix = "";
+                       const char* override_prefix = "";
 
-      g_dir_close (dir);
+                       {
+                               // See if we are in "sp" or "mp" mapping mode
+                               const char* gamemode = gamemode_get();
 
-                       // sort them
-                       dirlist = g_slist_sort (dirlist, vfsPakSort);
+                               if ( strcmp( gamemode, "sp" ) == 0 ) {
+                                       ignore_prefix = "mp_";
+                                       override_prefix = "sp_";
+                               }
+                               else if ( strcmp( gamemode, "mp" ) == 0 ) {
+                                       ignore_prefix = "sp_";
+                                       override_prefix = "mp_";
+                               }
+                       }
 
-                       // add the entries to the vfs and free the list
-                       while (dirlist)
+                       Archives archives;
+                       Archives archivesOverride;
+                       for (;; )
                        {
-                               GSList *cur = dirlist;
-                               char* name = (char*)cur->data;
+                               const char* name = g_dir_read_name( dir );
+                               if ( name == 0 ) {
+                                       break;
+                               }
 
-                               switch (iGameMode)
+                               for ( j = 0; j < g_numForbiddenDirs; ++j )
                                {
-                               case 1: // SP
-                                       if (strncmp(name,"mp_",3) == 0)
-                                       {
-                                               g_free (name);
-                                               dirlist = g_slist_remove (cur, name);
+                                       const char *p = strrchr( name, '/' );
+                                       p = ( p ? ( p + 1 ) : name );
+                                       if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
+                                               break;
+                                       }
+                               }
+                               if ( j < g_numForbiddenDirs ) {
+                                       continue;
+                               }
+
+                               const char *ext = strrchr( name, '.' );
+
+                               if ( ext && !string_compare_nocase_upper( ext, ".pk3dir" ) ) {
+                                       if ( g_numDirs == VFS_MAXDIRS ) {
                                                continue;
-                                       } else if (strncmp(name,"zz_",3) == 0)
-                                               memcpy(name,"sp",2);
-                                       break;
-                               case 2: // MP
-                                       if (strncmp(name,"sp_",3) == 0)
+                                       }
+                                       snprintf( g_strDirs[g_numDirs], PATH_MAX, "%s%s/", path, name );
+                                       g_strDirs[g_numDirs][PATH_MAX] = '\0';
+                                       FixDOSName( g_strDirs[g_numDirs] );
+                                       AddSlash( g_strDirs[g_numDirs] );
+                                       g_numDirs++;
+
                                        {
-                                               g_free (name);
-                                               dirlist = g_slist_remove (cur, name);
-                                               continue;
-                                       } else if (strncmp(name,"zz_",3) == 0)
-                                               memcpy(name,"mp",2);
-                                       break;
+                                               archive_entry_t entry;
+                                               entry.name = g_strDirs[g_numDirs - 1];
+                                               entry.archive = OpenArchive( g_strDirs[g_numDirs - 1] );
+                                               entry.is_pakfile = false;
+                                               g_archives.push_back( entry );
+                                       }
                                }
 
-                               sprintf (filename, "%s/%s", path, name);
-        vfsInitPakFile (filename);
+                               if ( ( ext == 0 ) || *( ++ext ) == '\0' || GetArchiveTable( archiveModules, ext ) == 0 ) {
+                                       continue;
+                               }
 
-                               g_free (name);
-                               dirlist = g_slist_remove (cur, name);
+                               // 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 ) {
+                                       archivesOverride.insert( name );
+                                       continue;
+                               }
+
+                               archives.insert( name );
+                       }
+
+                       g_dir_close( dir );
+
+                       // add the entries to the vfs
+                       for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
+                       {
+                               char filename[PATH_MAX];
+                               strcpy( filename, path );
+                               strcat( filename, ( *i ).c_str() );
+                               InitPakFile( archiveModules, filename );
+                       }
+                       for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
+                       {
+                               char filename[PATH_MAX];
+                               strcpy( filename, path );
+                               strcat( filename, ( *i ).c_str() );
+                               InitPakFile( archiveModules, filename );
                        }
-    }
-    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;
 }
 
-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;i<strlen(ret);i++)
-      {
-        if (ret[i]=='/')
-          count++;
-      }
-      // this is the clean, not short version
-      strcpy(out, in);
-      vfsCleanFileName(out);
-      for(i=0;i<=count;i++)
-      {
-        chunk = strrchr(out, '/');
-        if (backup)
-          backup[0] = '/';
-        chunk[0] = '\0';
-        backup = chunk;
-      }
-      return chunk+1;
-    }
-  }
-  return ret;
-}
-
-void vfsCleanFileName(char *in)
-{
-  strlwr(in);
-  vfsFixDOSName(in);
-  int n = strlen(in);
-  if (in[n-1] == '/')
-    in[n-1] = '\0';
-}
-
-// HYDRA: this now searches VFS/PAK files in addition to the filesystem
-// if FLAG is unspecified then ONLY dirs are searched.
-// PAK's are searched before DIRs to mimic engine behaviour
-// index is ignored when searching PAK files.
-// see ifilesystem.h
-char* vfsGetFullPath(const char *in, int index, int flag)
-{
-  int count = 0;
-  static char out[PATH_MAX];
-  char tmp[NAME_MAX];
-  int i;
-
-  if (flag & VFS_SEARCH_PAK)
-  {
-    char fixed[NAME_MAX];
-    GSList *lst;
-
-    strcpy (fixed, in);
-    vfsFixDOSName (fixed);
-    g_strdown (fixed);
-
-    for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))
-    {
-      VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
-
-      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/<fs_game>
-// 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(){
+       globalOutputStream() << "filesystem initialised\n";
+       g_observers.realise();
+}
+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<const char*>( ( *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<const char*>( ( *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;
+
+void FileSystem_Init(){
+}
+
+void FileSystem_Shutdown(){
+}
+
+VirtualFileSystem& GetFileSystem(){
+       return g_Quake3FileSystem;
+}