more eol-style
[xonotic/netradiant.git] / plugins / vfspak / 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 (/)
40 //
41 // Leonardo Zide (leo@lokigames.com)
42 //
43
44 #include <glib.h>
45 #include <stdio.h>
46 #if defined __linux__ || defined (__APPLE__)
47         #include <dirent.h>
48         #include <unistd.h>
49         #define WINAPI
50 #else
51         #include <wtypes.h>
52         #include <io.h>
53         #define R_OK 04
54         #define S_ISDIR(mode) (mode & _S_IFDIR)
55 #endif
56
57 #include "str.h"
58 #include <stdlib.h>
59 #include <sys/stat.h>
60 #include "vfs.h"
61 #include "vfspak.h"
62
63 typedef struct
64 {
65   char magic[4];         // Name of the new WAD format ("PACK")
66   gint32 diroffset;      // Position of WAD directory from start of file
67   gint32 dirsize;        // Number of entries * 0x40 (64 char)
68 } pakheader_t;
69
70 typedef struct
71 {
72   char filename[0x38];   // Name of the file, Unix style, with extension, 50 chars, padded with '\0'.
73   gint32 offset;         // Position of the entry in PACK file
74   gint32 size;           // Size of the entry in PACK file
75 } pakentry_t;
76
77 typedef struct
78 {
79   char*   name;
80   pakentry_t entry;
81   FILE *pak;
82 } VFS_PAKFILE;
83
84 // =============================================================================
85 // Global variables
86
87 static GSList* g_unzFiles;
88 static GSList* g_pakFiles;
89 static char    g_strDirs[VFS_MAXDIRS][PATH_MAX];
90 static int     g_numDirs;
91 static bool    g_bUsePak = true;
92
93 // =============================================================================
94 // Static functions
95
96 static void vfsAddSlash (char *str)
97 {
98   int n = strlen (str);
99   if (n > 0)
100   {
101     if (str[n-1] != '\\' && str[n-1] != '/')
102       strcat (str, "/");
103   }
104 }
105
106 static void vfsFixDOSName (char *src)
107 {
108   if (src == NULL)
109     return;
110
111   while (*src)
112   {
113     if (*src == '\\')
114       *src = '/';
115     src++;
116   }
117 }
118
119 static void vfsInitPakFile (const char *filename)
120 {
121   pakheader_t header;
122   FILE *f;
123   long i;
124
125   f = fopen (filename, "rb");
126   if (f == NULL)
127     return;
128
129   // read header
130   fread (header.magic, 1, 4, f);
131   fread (&header.diroffset, 1, 4, f);
132   fread (&header.dirsize, 1, 4, f);
133
134   // fix endianess
135   header.diroffset = GINT32_FROM_LE (header.diroffset);
136   header.dirsize = GINT32_FROM_LE (header.dirsize);
137
138   // check that the magic header
139   if (strncmp (header.magic, "PACK", 4))
140   {
141     fclose (f);
142     return;
143   }
144
145   g_FuncTable.m_pfnSysPrintf("  pak file: %s\n", filename);
146
147   g_unzFiles = g_slist_append (g_unzFiles, f);
148   fseek (f, header.diroffset, SEEK_SET);
149
150   for (i = 0; i < (long)(header.dirsize/sizeof (pakentry_t)); i++)
151   {
152     VFS_PAKFILE* file;
153
154     file = (VFS_PAKFILE*)g_malloc (sizeof (VFS_PAKFILE));
155     g_pakFiles = g_slist_append (g_pakFiles, file);
156
157     fread (file->entry.filename, 1, sizeof (file->entry.filename), f);
158     fread (&file->entry.offset, 1, sizeof (file->entry.offset), f);
159     fread (&file->entry.size, 1, sizeof (file->entry.size), f);
160     file->pak = f;
161
162     // fix endianess
163     file->entry.offset = GINT32_FROM_LE (file->entry.offset);
164     file->entry.size = GINT32_FROM_LE (file->entry.size);
165
166     // fix filename
167     vfsFixDOSName (file->entry.filename);
168     g_strdown (file->entry.filename);
169     //g_FuncTable.m_pfnSysPrintf("vfs file from pak: %s\n", file->entry.filename);
170   }
171 }
172
173 static GSList* vfsGetListInternal (const char *dir, const char *ext, bool directories)
174 {
175   GSList *lst, *lst_aux, *files = NULL;
176   char dirname[NAME_MAX], extension[NAME_MAX], filename[NAME_MAX];
177   int dirlen;
178   char *ptr;
179   //struct dirent *dirlist;
180   char *dirlist;
181   struct stat st;
182   GDir *diskdir;
183   int i;
184
185   dirname[0] = '\0';
186   if (dir != NULL)
187   {
188     strcat (dirname, dir);
189         g_strdown (dirname);
190         vfsFixDOSName (dirname);
191         vfsAddSlash (dirname);
192         Sys_Printf("vfs dirname_1: %s\n", dirname);
193   }
194   //else
195   //  dirname[0] = '\0';
196   dirlen = strlen (dirname);
197
198   if (ext != NULL)
199     strcpy (extension, ext);
200   else
201     extension[0] = '\0';
202   g_strdown (extension);
203
204   for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))
205   {
206     VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
207     gboolean found = FALSE;
208     ptr = file->entry.filename;
209
210     // check that the file name begins with dirname
211     for (i = 0; (*ptr && i < dirlen); i++, ptr++)
212       if (*ptr != dirname[i])
213         break;
214
215     if (i != dirlen)
216       continue;
217
218     if (directories)
219     {
220       char *sep = strchr (ptr, '/');
221       if (sep == NULL)
222         continue;
223
224       i = sep-ptr;
225
226       // check for duplicates
227       for (lst_aux = files; lst_aux; lst_aux = g_slist_next (lst_aux))
228         if (strncmp ((char*)lst_aux->data, ptr, i) == 0)
229         {
230           found = TRUE;
231           break;
232         }
233
234       if (!found)
235       {
236         char *name = g_strndup (ptr, i+1);
237         name[i] = '\0';
238         files = g_slist_append (files, name);
239       }
240     }
241     else
242     {
243       // check extension
244       if ((ext != NULL) && (strstr (ptr, extension) == NULL))
245         continue;
246
247       // check for duplicates
248       for (lst_aux = files; lst_aux; lst_aux = g_slist_next (lst_aux))
249         if (strcmp ((char*)lst_aux->data, ptr) == 0)
250         {
251           found = TRUE;
252           break;
253         }
254
255       if (!found)
256         files = g_slist_append (files, g_strdup (ptr));
257     }
258   }
259   
260   for (i = 0; i < g_numDirs; i++)
261   {
262     strcpy (dirname, g_strDirs[i]);
263     strcat (dirname, dir);
264     g_strdown (dirname);
265         vfsFixDOSName (dirname);
266         vfsAddSlash (dirname);
267
268     diskdir = g_dir_open (dirname, 0, NULL);
269
270         if (diskdir != NULL)
271     {
272       while (1)
273       {
274         const char* name = g_dir_read_name(diskdir);
275         if(name == NULL)
276           break;
277
278         if (directories && (name[0] == '.'))
279           continue;
280
281         sprintf (filename, "%s%s", dirname, name);
282         stat (filename, &st);
283                 Sys_Printf("vfs FileName: %s\n", filename);
284
285         if ((S_ISDIR (st.st_mode) != 0) != directories)
286           continue;
287
288         gboolean found = FALSE;
289
290         dirlist = g_strdup(name);
291
292         g_strdown (dirlist);
293
294         char *ptr_ext = strrchr (dirlist, '.');
295         if(ext == NULL
296           || (ext != NULL && ptr_ext != NULL && ptr_ext[0] != '\0' && strcmp (ptr_ext+1, extension) == 0))
297         {
298
299           // check for duplicates
300           for (lst_aux = files; lst_aux; lst_aux = g_slist_next (lst_aux))
301             if (strcmp ((char*)lst_aux->data, dirlist) == 0)
302             {
303               found = TRUE;
304               break;
305             }
306
307           if (!found)
308             files = g_slist_append (files, g_strdup (dirlist));
309         }
310
311         g_free(dirlist);
312       }
313       g_dir_close (diskdir);
314     }
315   }
316
317   return files;
318 }
319
320 /*!
321 This behaves identically to -stricmp(a,b), except that ASCII chars
322 [\]^`_ come AFTER alphabet chars instead of before. This is because
323 it effectively converts all alphabet chars to uppercase before comparison,
324 while stricmp converts them to lowercase.
325 */
326 //!\todo Analyse the code in rtcw/q3 to see how it behaves.
327 static int vfsPakSort (const void *a, const void *b)
328 {
329         char    *s1, *s2;
330         int             c1, c2;
331
332         s1 = (char*)a;
333         s2 = (char*)b;
334
335         do {
336                 c1 = *s1++;
337                 c2 = *s2++;
338
339                 if (c1 >= 'a' && c1 <= 'z')
340                 {
341                         c1 -= ('a' - 'A');
342                 }
343                 if (c2 >= 'a' && c2 <= 'z')
344                 {
345                         c2 -= ('a' - 'A');
346                 }
347
348                 if ( c1 == '\\' || c1 == ':' )
349                 {
350                         c1 = '/';
351                 }
352                 if ( c2 == '\\' || c2 == ':' )
353                 {
354                         c2 = '/';
355                 }
356
357                 // Arnout: note - sort pakfiles in reverse order. This ensures that
358                 // later pakfiles override earlier ones. This because the vfs module
359                 // returns a filehandle to the first file it can find (while it should
360                 // return the filehandle to the file in the most overriding pakfile, the
361                 // last one in the list that is).
362                 if (c1 < c2)
363                 {
364                         //return -1;            // strings not equal
365                         return 1;               // strings not equal
366                 }
367                 if (c1 > c2)
368                 {
369                         //return 1;
370                         return -1;
371                 }
372         } while (c1);
373
374         return 0;               // strings are equal
375 }
376
377 // =============================================================================
378 // Global functions
379
380 void vfsInitDirectory (const char *path)
381 {
382   char filename[PATH_MAX];
383   //struct dirent *direntry;
384   GDir *dir;
385   //GSList *dirlistptr;
386   GSList *dirlist = NULL;
387
388   if (g_numDirs == (VFS_MAXDIRS-1))
389     return;
390
391   strcpy (g_strDirs[g_numDirs], path);
392   vfsFixDOSName (g_strDirs[g_numDirs]);
393   vfsAddSlash (g_strDirs[g_numDirs]);
394   g_numDirs++;
395
396   if (g_bUsePak)
397   {
398     dir = g_dir_open (path, 0, NULL);
399     if (dir != NULL)
400     {
401       g_FuncTable.m_pfnSysPrintf("vfs directory: %s\n", path);
402
403           for(;;)
404       {
405         const char* name = g_dir_read_name(dir);
406         if(name == NULL)
407           break;
408
409         char *ext = strrchr (name, '.');
410         if ((ext == NULL) || (strcasecmp (ext, ".pak") != 0))
411           continue;
412
413         char* direntry = g_strdup(name);
414                 dirlist = g_slist_append (dirlist, direntry);
415       }
416
417       g_dir_close (dir);
418
419
420       // sort them
421       dirlist = g_slist_sort (dirlist, vfsPakSort);
422
423       // add the entries to the vfs and free the list
424       while (dirlist)
425       {
426         GSList *cur = dirlist;
427         char* name = (char*)cur->data;
428
429         sprintf (filename, "%s/%s", path, name);
430         vfsInitPakFile (filename);
431
432         g_free (name);
433         dirlist = g_slist_remove (cur, name);
434       }
435     } else
436           g_FuncTable.m_pfnSysFPrintf(SYS_WRN, "vfs directory not found: %s\n", path);
437
438   }
439 }
440
441
442 // frees all memory that we allocated
443 void vfsShutdown ()
444 {
445   while (g_unzFiles)
446   {
447     fclose ((FILE*)g_unzFiles->data);
448     g_unzFiles = g_slist_remove (g_unzFiles, g_unzFiles->data);
449   }
450
451   while (g_pakFiles)
452   {
453     g_free (g_pakFiles->data);
454     g_pakFiles = g_slist_remove (g_pakFiles, g_pakFiles->data);
455   }
456 }
457
458 GSList* vfsGetFileList (const char *dir, const char *ext)
459 {
460   return vfsGetListInternal (dir, ext, false);
461 }
462
463 GSList* vfsGetDirList (const char *dir)
464 {
465   return vfsGetListInternal (dir, NULL, true);
466 }
467
468 void vfsClearFileDirList (GSList **lst)
469 {
470   while (*lst)
471   {
472     g_free ((*lst)->data);
473     *lst = g_slist_remove (*lst, (*lst)->data);
474   }
475 }
476
477 // return the number of files that match
478 int vfsGetFileCount (const char *filename, int flag)
479 {
480   int i, count = 0;
481   char fixed[NAME_MAX], tmp[NAME_MAX];
482   GSList *lst;
483
484   strcpy (fixed, filename);
485   vfsFixDOSName (fixed);
486   g_strdown (fixed);
487
488   for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))
489   {
490     VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
491
492     if (strcmp (file->entry.filename, fixed) == 0)
493       count++;
494   }
495
496   for (i = 0; i < g_numDirs; i++)
497   {
498     strcpy (tmp, g_strDirs[i]);
499     strcat (tmp, fixed);
500     if (access (tmp, R_OK) == 0)
501       count++;
502   }
503
504   return count;
505 }
506
507 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
508 int vfsLoadFile (const char *filename, void **bufferptr, int index)
509 {
510   int i, count = 0;
511   char tmp[NAME_MAX], fixed[NAME_MAX];
512   GSList *lst;
513
514   *bufferptr = NULL;
515   strcpy (fixed, filename);
516   vfsFixDOSName (fixed);
517   g_strdown (fixed);
518
519   for (i = 0; i < g_numDirs; i++)
520   {
521     strcpy (tmp, g_strDirs[i]);
522     strcat (tmp, filename);
523     if (access (tmp, R_OK) == 0)
524     {
525       if (count == index)
526       {
527         long len;
528         FILE *f;
529
530         f = fopen (tmp, "rb");
531         if (f == NULL)
532           return -1;
533
534         fseek (f, 0, SEEK_END);
535         len = ftell (f);
536         rewind (f);
537
538         *bufferptr = malloc (len+1);
539         if (*bufferptr == NULL)
540           return -1;
541
542         fread (*bufferptr, 1, len, f);
543         fclose (f);
544
545         // we need to end the buffer with a 0
546         ((char*) (*bufferptr))[len] = 0;
547
548         return len;
549       }
550
551       count++;
552     }
553   }
554
555   for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))
556   {
557     VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
558
559     if (strcmp (file->entry.filename, fixed) != 0)
560       continue;
561
562     if (count == index)
563     {
564       fseek (file->pak, file->entry.offset, SEEK_SET);
565
566       *bufferptr = malloc (file->entry.size+1);
567       // we need to end the buffer with a 0
568       ((char*) (*bufferptr))[file->entry.size] = 0;
569
570       return fread (*bufferptr, 1, file->entry.size, file->pak);
571     }
572
573     count++;
574   }
575
576   return -1;
577 }
578
579 void vfsFreeFile (void *p)
580 {
581   g_free(p);
582 }
583
584 // open a full path file
585 int vfsLoadFullPathFile (const char *filename, void **bufferptr)
586 {
587   FILE *f;
588   long len;
589
590   f = fopen (filename, "rb");
591   if (f == NULL)
592     return -1;
593
594   fseek (f, 0, SEEK_END);
595   len = ftell (f);
596   rewind (f);
597
598   *bufferptr = g_malloc (len+1);
599   if (*bufferptr == NULL)
600     return -1;
601
602   fread (*bufferptr, 1, len, f);
603   fclose (f);
604
605   // we need to end the buffer with a 0
606   ((char*) (*bufferptr))[len] = 0;
607
608   return len;
609 }
610
611 void vfsCleanFileName(char *in)
612 {
613   strlwr(in);
614   vfsFixDOSName(in);
615   int n = strlen(in);
616   if (in[n-1] == '/')
617     in[n-1] = '\0';
618 }
619
620 const char* vfsBasePromptPath()
621 {
622 #ifdef _WIN32
623   static char* path = "C:";
624 #else
625   static char* path = "/";
626 #endif
627   return path;
628 }
629
630 /*!
631 \param shorten will try to match against the short version
632 http://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=144
633 recent switch back to short path names in project settings has broken some stuff
634 with shorten == true, we will convert in to short version before looking for root
635 FIXME WAAA .. the stuff below is much more simple on linux .. add appropriate #ifdef
636 */
637 char* vfsExtractRelativePath_short(const char *in, bool shorten)
638 {
639   int i;
640   char l_in[PATH_MAX];
641   char check[PATH_MAX];
642   static char out[PATH_MAX];
643   out[0] = 0;
644
645 #ifdef DBG_RLTPATH
646   Sys_Printf("vfsExtractRelativePath: %s\n", in);
647 #endif
648
649 #ifdef _WIN32
650   if (shorten)
651   {
652     // make it short
653     if (GetShortPathName(in, l_in, PATH_MAX) == 0)
654     {
655 #ifdef DBG_RLTPATH
656       Sys_Printf("GetShortPathName failed\n");
657 #endif
658       return NULL;
659     }
660   }
661   else
662   {
663     strcpy(l_in,in);
664   }
665   vfsCleanFileName(l_in);
666 #else
667   strcpy(l_in, in);
668   vfsCleanFileName(l_in);
669 #endif // ifdef WIN32
670
671
672 #ifdef DBG_RLTPATH
673   Sys_Printf("cleaned path: %s\n", l_in);
674 #endif
675
676   for (i = 0; i < g_numDirs; i++)
677   {
678     strcpy(check,g_strDirs[i]);
679     vfsCleanFileName(check);
680 #ifdef DBG_RLTPATH
681     Sys_Printf("Matching against %s\n", check);
682 #endif
683
684     // try to find a match
685     if (strstr(l_in, check))
686     {
687       strcpy(out,l_in+strlen(check)+1);
688       break;
689     }
690
691   }
692   if (out[0]!=0)
693   {
694 #ifdef DBG_RLTPATH
695     Sys_Printf("vfsExtractRelativePath: success\n");
696 #endif
697     return out;
698   }
699 #ifdef DBG_RLTPATH
700   Sys_Printf("vfsExtractRelativePath: failed\n");
701 #endif
702   return NULL;
703 }
704
705 // HYDRA: this now searches VFS/PAK files in addition to the filesystem
706 // if FLAG is unspecified then ONLY dirs are searched.
707 // PAK's are searched before DIRs to mimic engine behaviour
708 // index is ignored when searching PAK files.
709 // see ifilesystem.h
710 char* vfsGetFullPath(const char *in, int index, int flag)
711 {
712   int count = 0;
713   static char out[PATH_MAX];
714   char tmp[NAME_MAX];
715   int i;
716
717   if (flag & VFS_SEARCH_PAK)
718   {
719     char fixed[NAME_MAX];
720     GSList *lst;
721
722     strcpy (fixed, in);
723     vfsFixDOSName (fixed);
724     g_strdown (fixed);
725
726     for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))
727     {
728       VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
729
730       char *ptr,*lastptr;
731       lastptr = file->name;
732
733       while (ptr = strchr(lastptr,'/'))
734         lastptr = ptr+1;
735
736       if (strcmp (lastptr, fixed) == 0)
737       {
738         strncpy(out,file->name,PATH_MAX);
739         return out;
740       }
741     }
742
743   }
744
745   if (!flag || (flag & VFS_SEARCH_DIR))
746   {
747   for (i = 0; i < g_numDirs; i++)
748   {
749     strcpy (tmp, g_strDirs[i]);
750     strcat (tmp, in);
751     if (access (tmp, R_OK) == 0)
752     {
753       if (count == index)
754       {
755         strcpy (out, tmp);
756         return out;
757       }
758       count++;
759     }
760   }
761   }
762   return NULL;
763 }
764
765 // FIXME TTimo: this and the above should be merged at some point
766 char* vfsExtractRelativePath(const char *in)
767 {
768   static char out[PATH_MAX];
769   unsigned int i, count;
770   char *chunk, *backup = NULL; // those point to out stuff
771   char *ret = vfsExtractRelativePath_short(in, false);
772   if (!ret)
773   {
774 #ifdef DBG_RLTPATH
775     Sys_Printf("trying with a short version\n");
776 #endif
777     ret = vfsExtractRelativePath_short(in, true);
778     if (ret)
779     {
780       // ok, but we have a relative short version now
781       // hack the long relative version out of here
782       count = 0;
783       for(i=0;i<strlen(ret);i++)
784       {
785         if (ret[i]=='/')
786           count++;
787       }
788       // this is the clean, not short version
789       strcpy(out, in);
790       vfsCleanFileName(out);
791       for(i=0;i<=count;i++)
792       {
793         chunk = strrchr(out, '/');
794         if (backup)
795           backup[0] = '/';
796         chunk[0] = '\0';
797         backup = chunk;
798       }
799       return chunk+1;
800     }
801   }
802   return ret;
803 }