]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - plugins/vfspk3/vfs.cpp
bea0adb44079912412f58a091de3567d92a753e9
[xonotic/netradiant.git] / plugins / vfspk3 / vfs.cpp
1 /*
2 Copyright (c) 2001, Loki software, inc.
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without modification, 
6 are permitted provided that the following conditions are met:
7
8 Redistributions of source code must retain the above copyright notice, this list 
9 of conditions and the following disclaimer.
10
11 Redistributions in binary form must reproduce the above copyright notice, this
12 list of conditions and the following disclaimer in the documentation and/or
13 other materials provided with the distribution.
14
15 Neither the name of Loki software nor the names of its contributors may be used 
16 to endorse or promote products derived from this software without specific prior 
17 written permission. 
18
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' 
20 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
21 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
22 DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY 
23 DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
24 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
25 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
26 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
27 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
28 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
29 */
30
31 //
32 // Rules:
33 //
34 // - Directories should be searched in the following order: ~/.q3a/baseq3,
35 //   install dir (/usr/local/games/quake3/baseq3) and cd_path (/mnt/cdrom/baseq3).
36 //
37 // - Pak files are searched first inside the directories.
38 // - Case insensitive.
39 // - Unix-style slashes (/) (windows is backwards .. everyone knows that)
40 //
41 // Leonardo Zide (leo@lokigames.com)
42 //
43
44 #include "vfs.h"
45
46 #include <stdio.h>
47 #include <stdlib.h>
48 #include <glib/gslist.h>
49 #include <glib/gdir.h>
50 #include <glib/gstrfuncs.h>
51
52 #include "qerplugin.h"
53 #include "idatastream.h"
54 #include "iarchive.h"
55 ArchiveModules& FileSystemQ3API_getArchiveModules();
56 #include "ifilesystem.h"
57
58 #include "generic/callback.h"
59 #include "string/string.h"
60 #include "stream/stringstream.h"
61 #include "os/path.h"
62 #include "moduleobservers.h"
63 #include "filematch.h"
64
65
66 #define VFS_MAXDIRS 64
67
68 #if defined(WIN32)
69 #define PATH_MAX 260
70 #endif
71
72 #define gamemode_get GlobalRadiant().getGameMode
73
74
75
76 // =============================================================================
77 // Global variables
78
79 Archive* OpenArchive(const char* name);
80
81 struct archive_entry_t
82 {
83   CopiedString name;
84   Archive* archive;
85   bool is_pakfile;
86 };
87
88 #include <list>
89
90 typedef std::list<archive_entry_t> archives_t;
91
92 static archives_t g_archives;
93 static char    g_strDirs[VFS_MAXDIRS][PATH_MAX+1];
94 static int     g_numDirs;
95 static char    g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX+1];
96 static int     g_numForbiddenDirs = 0;
97 static bool    g_bUsePak = true;
98
99 ModuleObservers g_observers;
100
101 // =============================================================================
102 // Static functions
103
104 static void AddSlash (char *str)
105 {
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     const char *p = strrchr(directory, '/');
320     p = (p ? (p+1) : directory);
321     if(matchpattern(p, g_strForbiddenDirs[j], TRUE))
322       break;
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 }