Merge commit '830125fad042fad35dc029b6eb57c8156ad7e176'
[xonotic/netradiant.git] / plugins / vfspk3 / vfs.cpp
1 /*
2    Copyright (c) 2001, Loki software, inc.
3    All rights reserved.
4
5    Redistribution and use in source and binary forms, with or without modification,
6    are permitted provided that the following conditions are met:
7
8    Redistributions of source code must retain the above copyright notice, this list
9    of conditions and the following disclaimer.
10
11    Redistributions in binary form must reproduce the above copyright notice, this
12    list of conditions and the following disclaimer in the documentation and/or
13    other materials provided with the distribution.
14
15    Neither the name of Loki software nor the names of its contributors may be used
16    to endorse or promote products derived from this software without specific prior
17    written permission.
18
19    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
20    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22    DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
23    DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26    ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 //
32 // Rules:
33 //
34 // - Directories should be searched in the following order: ~/.q3a/baseq3,
35 //   install dir (/usr/local/games/quake3/baseq3) and cd_path (/mnt/cdrom/baseq3).
36 //
37 // - Pak files are searched first inside the directories.
38 // - Case insensitive.
39 // - Unix-style slashes (/) (windows is backwards .. everyone knows that)
40 //
41 // Leonardo Zide (leo@lokigames.com)
42 //
43
44 #include "vfs.h"
45
46 #include <stdio.h>
47 #include <stdlib.h>
48 #include <glib/gslist.h>
49 #include <glib/gdir.h>
50 #include <glib/gstrfuncs.h>
51
52 #include "qerplugin.h"
53 #include "idatastream.h"
54 #include "iarchive.h"
55 ArchiveModules& FileSystemQ3API_getArchiveModules();
56 #include "ifilesystem.h"
57
58 #include "generic/callback.h"
59 #include "string/string.h"
60 #include "stream/stringstream.h"
61 #include "os/path.h"
62 #include "moduleobservers.h"
63 #include "filematch.h"
64
65
66 #define VFS_MAXDIRS 64
67
68 #if defined( WIN32 )
69 #define PATH_MAX 260
70 #endif
71
72 #define gamemode_get GlobalRadiant().getGameMode
73
74
75
76 // =============================================================================
77 // Global variables
78
79 Archive* OpenArchive( const char* name );
80
81 struct archive_entry_t
82 {
83         CopiedString name;
84         Archive* archive;
85         bool is_pakfile;
86 };
87
88 #include <list>
89
90 typedef std::list<archive_entry_t> archives_t;
91
92 static archives_t g_archives;
93 static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
94 static int g_numDirs;
95 static char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
96 static int g_numForbiddenDirs = 0;
97 static bool g_bUsePak = true;
98
99 ModuleObservers g_observers;
100
101 // =============================================================================
102 // Static functions
103
104 static void AddSlash( char *str ){
105         std::size_t n = strlen( str );
106         if ( n > 0 ) {
107                 if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
108                         globalErrorStream() << "WARNING: directory path does not end with separator: " << str << "\n";
109                         strcat( str, "/" );
110                 }
111         }
112 }
113
114 static void FixDOSName( char *src ){
115         if ( src == 0 || strchr( src, '\\' ) == 0 ) {
116                 return;
117         }
118
119         globalErrorStream() << "WARNING: invalid path separator '\\': " << src << "\n";
120
121         while ( *src )
122         {
123                 if ( *src == '\\' ) {
124                         *src = '/';
125                 }
126                 src++;
127         }
128 }
129
130
131
132 const _QERArchiveTable* GetArchiveTable( ArchiveModules& archiveModules, const char* ext ){
133         StringOutputStream tmp( 16 );
134         tmp << LowerCase( ext );
135         return archiveModules.findModule( tmp.c_str() );
136 }
137 static void InitPakFile( ArchiveModules& archiveModules, const char *filename ){
138         const _QERArchiveTable* table = GetArchiveTable( archiveModules, path_get_extension( filename ) );
139
140         if ( table != 0 ) {
141                 archive_entry_t entry;
142                 entry.name = filename;
143
144                 entry.archive = table->m_pfnOpenArchive( filename );
145                 entry.is_pakfile = true;
146                 g_archives.push_back( entry );
147                 globalOutputStream() << "  pak file: " << filename << "\n";
148         }
149 }
150
151 inline void pathlist_prepend_unique( GSList*& pathlist, char* path ){
152         if ( g_slist_find_custom( pathlist, path, (GCompareFunc)path_compare ) == 0 ) {
153                 pathlist = g_slist_prepend( pathlist, path );
154         }
155         else
156         {
157                 g_free( path );
158         }
159 }
160
161 class DirectoryListVisitor : public Archive::Visitor
162 {
163 GSList*& m_matches;
164 const char* m_directory;
165 public:
166 DirectoryListVisitor( GSList*& matches, const char* directory )
167         : m_matches( matches ), m_directory( directory )
168 {}
169 void visit( const char* name ){
170         const char* subname = path_make_relative( name, m_directory );
171         if ( subname != name ) {
172                 if ( subname[0] == '/' ) {
173                         ++subname;
174                 }
175                 char* dir = g_strdup( subname );
176                 char* last_char = dir + strlen( dir );
177                 if ( last_char != dir && *( --last_char ) == '/' ) {
178                         *last_char = '\0';
179                 }
180                 pathlist_prepend_unique( m_matches, dir );
181         }
182 }
183 };
184
185 class FileListVisitor : public Archive::Visitor
186 {
187 GSList*& m_matches;
188 const char* m_directory;
189 const char* m_extension;
190 public:
191 FileListVisitor( GSList*& matches, const char* directory, const char* extension )
192         : m_matches( matches ), m_directory( directory ), m_extension( extension )
193 {}
194 void visit( const char* name ){
195         const char* subname = path_make_relative( name, m_directory );
196         if ( subname != name ) {
197                 if ( subname[0] == '/' ) {
198                         ++subname;
199                 }
200                 if ( m_extension[0] == '*' || extension_equal( path_get_extension( subname ), m_extension ) ) {
201                         pathlist_prepend_unique( m_matches, g_strdup( subname ) );
202                 }
203         }
204 }
205 };
206
207 static GSList* GetListInternal( const char *refdir, const char *ext, bool directories, std::size_t depth ){
208         GSList* files = 0;
209
210         ASSERT_MESSAGE( refdir[strlen( refdir ) - 1] == '/', "search path does not end in '/'" );
211
212         if ( directories ) {
213                 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
214                 {
215                         DirectoryListVisitor visitor( files, refdir );
216                         ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eDirectories, depth ), refdir );
217                 }
218         }
219         else
220         {
221                 for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
222                 {
223                         FileListVisitor visitor( files, refdir, ext );
224                         ( *i ).archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, depth ), refdir );
225                 }
226         }
227
228         files = g_slist_reverse( files );
229
230         return files;
231 }
232
233 inline int ascii_to_upper( int c ){
234         if ( c >= 'a' && c <= 'z' ) {
235                 return c - ( 'a' - 'A' );
236         }
237         return c;
238 }
239
240 /*!
241    This behaves identically to stricmp(a,b), except that ASCII chars
242    [\]^`_ come AFTER alphabet chars instead of before. This is because
243    it converts all alphabet chars to uppercase before comparison,
244    while stricmp converts them to lowercase.
245  */
246 static int string_compare_nocase_upper( const char* a, const char* b ){
247         for (;; )
248         {
249                 int c1 = ascii_to_upper( *a++ );
250                 int c2 = ascii_to_upper( *b++ );
251
252                 if ( c1 < c2 ) {
253                         return -1; // a < b
254                 }
255                 if ( c1 > c2 ) {
256                         return 1; // a > b
257                 }
258                 if ( c1 == 0 ) {
259                         return 0; // a == b
260                 }
261         }
262 }
263
264 // Arnout: note - sort pakfiles in reverse order. This ensures that
265 // later pakfiles override earlier ones. This because the vfs module
266 // returns a filehandle to the first file it can find (while it should
267 // return the filehandle to the file in the most overriding pakfile, the
268 // last one in the list that is).
269
270 //!\todo Analyse the code in rtcw/q3 to see which order it sorts pak files.
271 class PakLess
272 {
273 public:
274 bool operator()( const CopiedString& self, const CopiedString& other ) const {
275         return string_compare_nocase_upper( self.c_str(), other.c_str() ) > 0;
276 }
277 };
278
279 typedef std::set<CopiedString, PakLess> Archives;
280
281 // =============================================================================
282 // Global functions
283
284 // reads all pak files from a dir
285 void InitDirectory( const char* directory, ArchiveModules& archiveModules ){
286         int j;
287
288         g_numForbiddenDirs = 0;
289         StringTokeniser st( GlobalRadiant().getGameDescriptionKeyValue( "forbidden_paths" ), " " );
290         for ( j = 0; j < VFS_MAXDIRS; ++j )
291         {
292                 const char *t = st.getToken();
293                 if ( string_empty( t ) ) {
294                         break;
295                 }
296                 strncpy( g_strForbiddenDirs[g_numForbiddenDirs], t, PATH_MAX );
297                 g_strForbiddenDirs[g_numForbiddenDirs][PATH_MAX] = '\0';
298                 ++g_numForbiddenDirs;
299         }
300
301         for ( j = 0; j < g_numForbiddenDirs; ++j )
302         {
303                 char* dbuf = g_strdup( directory );
304                 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
305                         dbuf[strlen( dbuf ) - 1] = 0;
306                 }
307                 const char *p = strrchr( dbuf, '/' );
308                 p = ( p ? ( p + 1 ) : dbuf );
309                 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
310                         g_free( dbuf );
311                         break;
312                 }
313                 g_free( dbuf );
314         }
315         if ( j < g_numForbiddenDirs ) {
316                 printf( "Directory %s matched by forbidden dirs, removed\n", directory );
317                 return;
318         }
319
320         if ( g_numDirs == VFS_MAXDIRS ) {
321                 return;
322         }
323
324         strncpy( g_strDirs[g_numDirs], directory, PATH_MAX );
325         g_strDirs[g_numDirs][PATH_MAX] = '\0';
326         FixDOSName( g_strDirs[g_numDirs] );
327         AddSlash( g_strDirs[g_numDirs] );
328
329         const char* path = g_strDirs[g_numDirs];
330
331         g_numDirs++;
332
333         {
334                 archive_entry_t entry;
335                 entry.name = path;
336                 entry.archive = OpenArchive( path );
337                 entry.is_pakfile = false;
338                 g_archives.push_back( entry );
339         }
340
341         if ( g_bUsePak ) {
342                 GDir* dir = g_dir_open( path, 0, 0 );
343
344                 if ( dir != 0 ) {
345                         globalOutputStream() << "vfs directory: " << path << "\n";
346
347                         const char* ignore_prefix = "";
348                         const char* override_prefix = "";
349
350                         {
351                                 // See if we are in "sp" or "mp" mapping mode
352                                 const char* gamemode = gamemode_get();
353
354                                 if ( strcmp( gamemode, "sp" ) == 0 ) {
355                                         ignore_prefix = "mp_";
356                                         override_prefix = "sp_";
357                                 }
358                                 else if ( strcmp( gamemode, "mp" ) == 0 ) {
359                                         ignore_prefix = "sp_";
360                                         override_prefix = "mp_";
361                                 }
362                         }
363
364                         Archives archives;
365                         Archives archivesOverride;
366                         for (;; )
367                         {
368                                 const char* name = g_dir_read_name( dir );
369                                 if ( name == 0 ) {
370                                         break;
371                                 }
372
373                                 for ( j = 0; j < g_numForbiddenDirs; ++j )
374                                 {
375                                         const char *p = strrchr( name, '/' );
376                                         p = ( p ? ( p + 1 ) : name );
377                                         if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
378                                                 break;
379                                         }
380                                 }
381                                 if ( j < g_numForbiddenDirs ) {
382                                         continue;
383                                 }
384
385                                 const char *ext = strrchr( name, '.' );
386
387                                 if ( ext && !string_compare_nocase_upper( ext, ".pk3dir" ) ) {
388                                         if ( g_numDirs == VFS_MAXDIRS ) {
389                                                 continue;
390                                         }
391                                         snprintf( g_strDirs[g_numDirs], PATH_MAX, "%s%s/", path, name );
392                                         g_strDirs[g_numDirs][PATH_MAX] = '\0';
393                                         FixDOSName( g_strDirs[g_numDirs] );
394                                         AddSlash( g_strDirs[g_numDirs] );
395                                         g_numDirs++;
396
397                                         {
398                                                 archive_entry_t entry;
399                                                 entry.name = g_strDirs[g_numDirs - 1];
400                                                 entry.archive = OpenArchive( g_strDirs[g_numDirs - 1] );
401                                                 entry.is_pakfile = false;
402                                                 g_archives.push_back( entry );
403                                         }
404                                 }
405
406                                 if ( ( ext == 0 ) || *( ++ext ) == '\0' || GetArchiveTable( archiveModules, ext ) == 0 ) {
407                                         continue;
408                                 }
409
410                                 // using the same kludge as in engine to ensure consistency
411                                 if ( !string_empty( ignore_prefix ) && strncmp( name, ignore_prefix, strlen( ignore_prefix ) ) == 0 ) {
412                                         continue;
413                                 }
414                                 if ( !string_empty( override_prefix ) && strncmp( name, override_prefix, strlen( override_prefix ) ) == 0 ) {
415                                         archivesOverride.insert( name );
416                                         continue;
417                                 }
418
419                                 archives.insert( name );
420                         }
421
422                         g_dir_close( dir );
423
424                         // add the entries to the vfs
425                         for ( Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i )
426                         {
427                                 char filename[PATH_MAX];
428                                 strcpy( filename, path );
429                                 strcat( filename, ( *i ).c_str() );
430                                 InitPakFile( archiveModules, filename );
431                         }
432                         for ( Archives::iterator i = archives.begin(); i != archives.end(); ++i )
433                         {
434                                 char filename[PATH_MAX];
435                                 strcpy( filename, path );
436                                 strcat( filename, ( *i ).c_str() );
437                                 InitPakFile( archiveModules, filename );
438                         }
439                 }
440                 else
441                 {
442                         globalErrorStream() << "vfs directory not found: " << path << "\n";
443                 }
444         }
445 }
446
447 // frees all memory that we allocated
448 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
449 //   (for instance when modifying the project settings)
450 void Shutdown(){
451         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
452         {
453                 ( *i ).archive->release();
454         }
455         g_archives.clear();
456
457         g_numDirs = 0;
458         g_numForbiddenDirs = 0;
459 }
460
461 #define VFS_SEARCH_PAK 0x1
462 #define VFS_SEARCH_DIR 0x2
463
464 int GetFileCount( const char *filename, int flag ){
465         int count = 0;
466         char fixed[PATH_MAX + 1];
467
468         strncpy( fixed, filename, PATH_MAX );
469         fixed[PATH_MAX] = '\0';
470         FixDOSName( fixed );
471
472         if ( !flag ) {
473                 flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
474         }
475
476         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
477         {
478                 if ( ( *i ).is_pakfile && ( flag & VFS_SEARCH_PAK ) != 0
479                          || !( *i ).is_pakfile && ( flag & VFS_SEARCH_DIR ) != 0 ) {
480                         if ( ( *i ).archive->containsFile( fixed ) ) {
481                                 ++count;
482                         }
483                 }
484         }
485
486         return count;
487 }
488
489 ArchiveFile* OpenFile( const char* filename ){
490         ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
491         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
492         {
493                 ArchiveFile* file = ( *i ).archive->openFile( filename );
494                 if ( file != 0 ) {
495                         return file;
496                 }
497         }
498
499         return 0;
500 }
501
502 ArchiveTextFile* OpenTextFile( const char* filename ){
503         ASSERT_MESSAGE( strchr( filename, '\\' ) == 0, "path contains invalid separator '\\': \"" << filename << "\"" );
504         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
505         {
506                 ArchiveTextFile* file = ( *i ).archive->openTextFile( filename );
507                 if ( file != 0 ) {
508                         return file;
509                 }
510         }
511
512         return 0;
513 }
514
515 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
516 std::size_t LoadFile( const char *filename, void **bufferptr, int index ){
517         char fixed[PATH_MAX + 1];
518
519         strncpy( fixed, filename, PATH_MAX );
520         fixed[PATH_MAX] = '\0';
521         FixDOSName( fixed );
522
523         ArchiveFile* file = OpenFile( fixed );
524
525         if ( file != 0 ) {
526                 *bufferptr = malloc( file->size() + 1 );
527                 // we need to end the buffer with a 0
528                 ( (char*) ( *bufferptr ) )[file->size()] = 0;
529
530                 std::size_t length = file->getInputStream().read( (InputStream::byte_type*)*bufferptr, file->size() );
531                 file->release();
532                 return length;
533         }
534
535         *bufferptr = 0;
536         return 0;
537 }
538
539 void FreeFile( void *p ){
540         free( p );
541 }
542
543 GSList* GetFileList( const char *dir, const char *ext, std::size_t depth ){
544         return GetListInternal( dir, ext, false, depth );
545 }
546
547 GSList* GetDirList( const char *dir, std::size_t depth ){
548         return GetListInternal( dir, 0, true, depth );
549 }
550
551 void ClearFileDirList( GSList **lst ){
552         while ( *lst )
553         {
554                 g_free( ( *lst )->data );
555                 *lst = g_slist_remove( *lst, ( *lst )->data );
556         }
557 }
558
559 const char* FindFile( const char* relative ){
560         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
561         {
562                 if ( ( *i ).archive->containsFile( relative ) ) {
563                         return ( *i ).name.c_str();
564                 }
565         }
566
567         return "";
568 }
569
570 const char* FindPath( const char* absolute ){
571         const char *best = "";
572         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
573         {
574                 if ( string_length( ( *i ).name.c_str() ) > string_length( best ) ) {
575                         if ( path_equal_n( absolute, ( *i ).name.c_str(), string_length( ( *i ).name.c_str() ) ) ) {
576                                 best = ( *i ).name.c_str();
577                         }
578                 }
579         }
580
581         return best;
582 }
583
584
585 class Quake3FileSystem : public VirtualFileSystem
586 {
587 public:
588 void initDirectory( const char *path ){
589         InitDirectory( path, FileSystemQ3API_getArchiveModules() );
590 }
591 void initialise(){
592         globalOutputStream() << "filesystem initialised\n";
593         g_observers.realise();
594 }
595 void shutdown(){
596         g_observers.unrealise();
597         globalOutputStream() << "filesystem shutdown\n";
598         Shutdown();
599 }
600
601 int getFileCount( const char *filename, int flags ){
602         return GetFileCount( filename, flags );
603 }
604 ArchiveFile* openFile( const char* filename ){
605         return OpenFile( filename );
606 }
607 ArchiveTextFile* openTextFile( const char* filename ){
608         return OpenTextFile( filename );
609 }
610 std::size_t loadFile( const char *filename, void **buffer ){
611         return LoadFile( filename, buffer, 0 );
612 }
613 void freeFile( void *p ){
614         FreeFile( p );
615 }
616
617 void forEachDirectory( const char* basedir, const FileNameCallback& callback, std::size_t depth ){
618         GSList* list = GetDirList( basedir, depth );
619
620         for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
621         {
622                 callback( reinterpret_cast<const char*>( ( *i ).data ) );
623         }
624
625         ClearFileDirList( &list );
626 }
627 void forEachFile( const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth ){
628         GSList* list = GetFileList( basedir, extension, depth );
629
630         for ( GSList* i = list; i != 0; i = g_slist_next( i ) )
631         {
632                 const char* name = reinterpret_cast<const char*>( ( *i ).data );
633                 if ( extension_equal( path_get_extension( name ), extension ) ) {
634                         callback( name );
635                 }
636         }
637
638         ClearFileDirList( &list );
639 }
640 GSList* getDirList( const char *basedir ){
641         return GetDirList( basedir, 1 );
642 }
643 GSList* getFileList( const char *basedir, const char *extension ){
644         return GetFileList( basedir, extension, 1 );
645 }
646 void clearFileDirList( GSList **lst ){
647         ClearFileDirList( lst );
648 }
649
650 const char* findFile( const char *name ){
651         return FindFile( name );
652 }
653 const char* findRoot( const char *name ){
654         return FindPath( name );
655 }
656
657 void attach( ModuleObserver& observer ){
658         g_observers.attach( observer );
659 }
660 void detach( ModuleObserver& observer ){
661         g_observers.detach( observer );
662 }
663
664 Archive* getArchive( const char* archiveName, bool pakonly ){
665         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
666         {
667                 if ( pakonly && !( *i ).is_pakfile ) {
668                         continue;
669                 }
670
671                 if ( path_equal( ( *i ).name.c_str(), archiveName ) ) {
672                         return ( *i ).archive;
673                 }
674         }
675         return 0;
676 }
677 void forEachArchive( const ArchiveNameCallback& callback, bool pakonly, bool reverse ){
678         if ( reverse ) {
679                 g_archives.reverse();
680         }
681
682         for ( archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i )
683         {
684                 if ( pakonly && !( *i ).is_pakfile ) {
685                         continue;
686                 }
687
688                 callback( ( *i ).name.c_str() );
689         }
690
691         if ( reverse ) {
692                 g_archives.reverse();
693         }
694 }
695 };
696
697 Quake3FileSystem g_Quake3FileSystem;
698
699 void FileSystem_Init(){
700 }
701
702 void FileSystem_Shutdown(){
703 }
704
705 VirtualFileSystem& GetFileSystem(){
706         return g_Quake3FileSystem;
707 }