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