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