vfspk3 in q3map2: also support -fs_forbiddenpath
[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     printf("match against %s?\n", g_strForbiddenDirs[j]);
319     if(!string_compare_nocase_upper(directory, g_strForbiddenDirs[j])
320     || (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])))
321       break;
322     printf("not matched\n");
323   }
324   if(j < g_numForbiddenDirs)
325   {
326     printf("Directory %s matched by forbidden dirs, removed\n", directory);
327     return;
328   }
329
330   if (g_numDirs == VFS_MAXDIRS)
331     return;
332
333   strncpy(g_strDirs[g_numDirs], directory, PATH_MAX);
334   g_strDirs[g_numDirs][PATH_MAX] = '\0';
335   FixDOSName (g_strDirs[g_numDirs]);
336   AddSlash (g_strDirs[g_numDirs]);
337
338   const char* path = g_strDirs[g_numDirs];
339   
340   g_numDirs++;
341
342   {
343     archive_entry_t entry;
344     entry.name = path;
345     entry.archive = OpenArchive(path);
346     entry.is_pakfile = false;
347     g_archives.push_back(entry);
348   }
349
350   if (g_bUsePak)
351   {
352     GDir* dir = g_dir_open (path, 0, 0);
353
354     if (dir != 0)
355     {
356                         globalOutputStream() << "vfs directory: " << path << "\n";
357
358       const char* ignore_prefix = "";
359       const char* override_prefix = "";
360
361       {
362         // See if we are in "sp" or "mp" mapping mode
363         const char* gamemode = gamemode_get();
364
365                     if (strcmp (gamemode, "sp") == 0)
366         {
367                                   ignore_prefix = "mp_";
368           override_prefix = "sp_";
369         }
370                     else if (strcmp (gamemode, "mp") == 0)
371         {
372                                   ignore_prefix = "sp_";
373           override_prefix = "mp_";
374         }
375       }
376
377       Archives archives;
378       Archives archivesOverride;
379       for(;;)
380       {
381         const char* name = g_dir_read_name(dir);
382         if(name == 0)
383           break;
384
385         for(j = 0; j < g_numForbiddenDirs; ++j)
386           if(!string_compare_nocase_upper(name, g_strForbiddenDirs[j]))
387             break;
388         if(j < g_numForbiddenDirs)
389           continue;
390
391         const char *ext = strrchr (name, '.');
392
393         if(ext && !string_compare_nocase_upper(ext, ".pk3dir"))
394         {
395           if (g_numDirs == VFS_MAXDIRS)
396             continue;
397           snprintf(g_strDirs[g_numDirs], PATH_MAX, "%s%s/", path, name);
398           g_strDirs[g_numDirs][PATH_MAX] = '\0';
399           FixDOSName (g_strDirs[g_numDirs]);
400           AddSlash (g_strDirs[g_numDirs]);
401           g_numDirs++;
402
403           {
404             archive_entry_t entry;
405             entry.name = g_strDirs[g_numDirs-1];
406             entry.archive = OpenArchive(g_strDirs[g_numDirs-1]);
407             entry.is_pakfile = false;
408             g_archives.push_back(entry);
409           }
410         }
411
412         if ((ext == 0) || *(++ext) == '\0' || GetArchiveTable(archiveModules, ext) == 0)
413           continue;
414
415         // using the same kludge as in engine to ensure consistency
416                                 if(!string_empty(ignore_prefix) && strncmp(name, ignore_prefix, strlen(ignore_prefix)) == 0)
417                                 {
418                                         continue;
419                                 }
420                                 if(!string_empty(override_prefix) && strncmp(name, override_prefix, strlen(override_prefix)) == 0)
421         {
422           archivesOverride.insert(name);
423                                         continue;
424         }
425
426         archives.insert(name);
427       }
428
429       g_dir_close (dir);
430
431                         // add the entries to the vfs
432       for(Archives::iterator i = archivesOverride.begin(); i != archivesOverride.end(); ++i)
433                         {
434         char filename[PATH_MAX];
435         strcpy(filename, path);
436         strcat(filename, (*i).c_str());
437         InitPakFile(archiveModules, filename);
438                         }
439       for(Archives::iterator i = archives.begin(); i != archives.end(); ++i)
440                         {
441         char filename[PATH_MAX];
442         strcpy(filename, path);
443         strcat(filename, (*i).c_str());
444         InitPakFile(archiveModules, filename);
445                         }
446     }
447     else
448     {
449       globalErrorStream() << "vfs directory not found: " << path << "\n";
450     }
451   }
452 }
453
454 // frees all memory that we allocated
455 // FIXME TTimo this should be improved so that we can shutdown and restart the VFS without exiting Radiant?
456 //   (for instance when modifying the project settings)
457 void Shutdown()
458 {
459   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
460   {
461     (*i).archive->release();
462   }
463   g_archives.clear();
464
465   g_numDirs = 0;
466   g_numForbiddenDirs = 0;
467 }
468
469 #define VFS_SEARCH_PAK 0x1
470 #define VFS_SEARCH_DIR 0x2
471
472 int GetFileCount (const char *filename, int flag)
473 {
474   int count = 0;
475   char fixed[PATH_MAX+1];
476
477   strncpy(fixed, filename, PATH_MAX);
478   fixed[PATH_MAX] = '\0';
479   FixDOSName (fixed);
480
481   if(!flag)
482     flag = VFS_SEARCH_PAK | VFS_SEARCH_DIR;
483
484   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
485   {
486     if((*i).is_pakfile && (flag & VFS_SEARCH_PAK) != 0
487       || !(*i).is_pakfile && (flag & VFS_SEARCH_DIR) != 0)
488     {
489       if((*i).archive->containsFile(fixed))
490         ++count;
491     }
492   }
493
494   return count;
495 }
496
497 ArchiveFile* OpenFile(const char* filename)
498 {
499   ASSERT_MESSAGE(strchr(filename, '\\') == 0, "path contains invalid separator '\\': \"" << filename << "\""); 
500   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
501   {
502     ArchiveFile* file = (*i).archive->openFile(filename);
503     if(file != 0)
504     {
505       return file;
506     }
507   }
508
509   return 0;
510 }
511
512 ArchiveTextFile* OpenTextFile(const char* filename)
513 {
514   ASSERT_MESSAGE(strchr(filename, '\\') == 0, "path contains invalid separator '\\': \"" << filename << "\""); 
515   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
516   {
517     ArchiveTextFile* file = (*i).archive->openTextFile(filename);
518     if(file != 0)
519     {
520       return file;
521     }
522   }
523
524   return 0;
525 }
526
527 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
528 std::size_t LoadFile (const char *filename, void **bufferptr, int index)
529 {
530   char fixed[PATH_MAX+1];
531
532   strncpy (fixed, filename, PATH_MAX);
533   fixed[PATH_MAX] = '\0';
534   FixDOSName (fixed);
535
536   ArchiveFile* file = OpenFile(fixed);
537   
538   if(file != 0)
539   {
540     *bufferptr = malloc (file->size()+1);
541     // we need to end the buffer with a 0
542     ((char*) (*bufferptr))[file->size()] = 0;
543
544     std::size_t length = file->getInputStream().read((InputStream::byte_type*)*bufferptr, file->size());
545     file->release();
546     return length;
547   }
548
549   *bufferptr = 0;
550   return 0;
551 }
552
553 void FreeFile (void *p)
554 {
555   free(p);
556 }
557
558 GSList* GetFileList (const char *dir, const char *ext, std::size_t depth)
559 {
560   return GetListInternal (dir, ext, false, depth);
561 }
562
563 GSList* GetDirList (const char *dir, std::size_t depth)
564 {
565   return GetListInternal (dir, 0, true, depth);
566 }
567
568 void ClearFileDirList (GSList **lst)
569 {
570   while (*lst)
571   {
572     g_free ((*lst)->data);
573     *lst = g_slist_remove (*lst, (*lst)->data);
574   }
575 }
576     
577 const char* FindFile(const char* relative)
578 {
579   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
580   {
581     if((*i).archive->containsFile(relative))
582     {
583       return (*i).name.c_str();
584     }
585   }
586
587   return "";
588 }
589
590 const char* FindPath(const char* absolute)
591 {
592   const char *best = "";
593   for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
594   {
595         if(string_length((*i).name.c_str()) > string_length(best))
596       if(path_equal_n(absolute, (*i).name.c_str(), string_length((*i).name.c_str())))
597         best = (*i).name.c_str();
598   }
599
600   return best;
601 }
602
603
604 class Quake3FileSystem : public VirtualFileSystem
605 {
606 public:
607   void initDirectory(const char *path)
608   {
609     InitDirectory(path, FileSystemQ3API_getArchiveModules());
610   }
611   void initialise()
612   {
613     globalOutputStream() << "filesystem initialised\n";
614     g_observers.realise();
615   }
616   void shutdown()
617   {
618     g_observers.unrealise();
619     globalOutputStream() << "filesystem shutdown\n";
620     Shutdown();
621   }
622
623   int getFileCount(const char *filename, int flags)
624   {
625     return GetFileCount(filename, flags);
626   }
627   ArchiveFile* openFile(const char* filename)
628   {
629     return OpenFile(filename);
630   }
631   ArchiveTextFile* openTextFile(const char* filename)
632   {
633     return OpenTextFile(filename);
634   }
635   std::size_t loadFile(const char *filename, void **buffer)
636   {
637     return LoadFile(filename, buffer, 0);
638   }
639   void freeFile(void *p)
640   {
641     FreeFile(p);
642   }
643
644   void forEachDirectory(const char* basedir, const FileNameCallback& callback, std::size_t depth)
645   {
646     GSList* list = GetDirList(basedir, depth);
647
648     for(GSList* i = list; i != 0; i = g_slist_next(i))
649     {
650       callback(reinterpret_cast<const char*>((*i).data));
651     }
652
653     ClearFileDirList(&list);
654   }
655   void forEachFile(const char* basedir, const char* extension, const FileNameCallback& callback, std::size_t depth)
656   {
657     GSList* list = GetFileList(basedir, extension, depth);
658
659     for(GSList* i = list; i != 0; i = g_slist_next(i))
660     {
661       const char* name = reinterpret_cast<const char*>((*i).data);
662       if(extension_equal(path_get_extension(name), extension))
663       {
664         callback(name);
665       }
666     }
667
668     ClearFileDirList(&list);
669   }
670   GSList* getDirList(const char *basedir)
671   {
672     return GetDirList(basedir, 1);
673   }
674   GSList* getFileList(const char *basedir, const char *extension)
675   {
676     return GetFileList(basedir, extension, 1);
677   }
678   void clearFileDirList(GSList **lst)
679   {
680     ClearFileDirList(lst);
681   }
682
683   const char* findFile(const char *name)
684   {
685     return FindFile(name);
686   }
687   const char* findRoot(const char *name)
688   {
689     return FindPath(name);
690   }
691
692   void attach(ModuleObserver& observer)
693   {
694     g_observers.attach(observer);
695   }
696   void detach(ModuleObserver& observer)
697   {
698     g_observers.detach(observer);
699   }
700
701   Archive* getArchive(const char* archiveName, bool pakonly)
702   {
703     for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
704     {
705       if(pakonly && !(*i).is_pakfile)
706         continue;
707
708       if(path_equal((*i).name.c_str(), archiveName))
709         return (*i).archive;
710     }
711     return 0;
712   }
713   void forEachArchive(const ArchiveNameCallback& callback, bool pakonly, bool reverse)
714   {
715     if (reverse)
716       g_archives.reverse();
717
718     for(archives_t::iterator i = g_archives.begin(); i != g_archives.end(); ++i)
719     {
720       if(pakonly && !(*i).is_pakfile)
721         continue;
722
723       callback((*i).name.c_str());
724     }
725
726     if (reverse)
727       g_archives.reverse();
728   }
729 };
730
731 Quake3FileSystem g_Quake3FileSystem;
732
733 void FileSystem_Init()
734 {
735 }
736
737 void FileSystem_Shutdown()
738 {
739 }
740
741 VirtualFileSystem& GetFileSystem()
742 {
743   return g_Quake3FileSystem;
744 }