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