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