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