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