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