q3map2: increase VFS_MAXDIRS because multiple pakpaths with many pk3dirs can lead...
[xonotic/netradiant.git] / tools / quake3 / common / vfs.c
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 <string.h>
45 #include <stdlib.h>
46 #include <sys/stat.h>
47
48 #include "cmdlib.h"
49 #include "filematch.h"
50 #include "mathlib.h"
51 #include "inout.h"
52 #include "vfs.h"
53 #include <unzip.h>
54 #include <glib.h>
55
56 typedef struct
57 {
58         char*   name;
59         unzFile zipfile;
60         unz_file_pos zippos;
61         guint32 size;
62 } VFS_PAKFILE;
63
64 // =============================================================================
65 // Global variables
66
67 static GSList*  g_unzFiles;
68 static GSList*  g_pakFiles;
69 static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
70 static int g_numDirs;
71 char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
72 int g_numForbiddenDirs = 0;
73 static gboolean g_bUsePak = TRUE;
74
75 // =============================================================================
76 // Static functions
77
78 static void vfsAddSlash( char *str ){
79         int n = strlen( str );
80         if ( n > 0 ) {
81                 if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
82                         strcat( str, "/" );
83                 }
84         }
85 }
86
87 static void vfsFixDOSName( char *src ){
88         if ( src == NULL ) {
89                 return;
90         }
91
92         while ( *src )
93         {
94                 if ( *src == '\\' ) {
95                         *src = '/';
96                 }
97                 src++;
98         }
99 }
100
101 //!\todo Define globally or use heap-allocated string.
102 #define NAME_MAX 255
103
104 static void vfsInitPakFile( const char *filename ){
105         unz_global_info gi;
106         unzFile uf;
107         guint32 i;
108         int err;
109
110         uf = unzOpen( filename );
111         if ( uf == NULL ) {
112                 return;
113         }
114
115         g_unzFiles = g_slist_append( g_unzFiles, uf );
116
117         err = unzGetGlobalInfo( uf,&gi );
118         if ( err != UNZ_OK ) {
119                 return;
120         }
121         unzGoToFirstFile( uf );
122
123         for ( i = 0; i < gi.number_entry; i++ )
124         {
125                 char filename_inzip[NAME_MAX];
126                 char *filename_lower;
127                 unz_file_info file_info;
128                 VFS_PAKFILE* file;
129
130                 err = unzGetCurrentFileInfo( uf, &file_info, filename_inzip, sizeof( filename_inzip ), NULL, 0, NULL, 0 );
131                 if ( err != UNZ_OK ) {
132                         break;
133                 }
134                 unz_file_pos pos;
135                 err = unzGetFilePos( uf, &pos );
136                 if ( err != UNZ_OK ) {
137                         break;
138                 }
139
140                 file = (VFS_PAKFILE*)safe_malloc( sizeof( VFS_PAKFILE ) );
141                 g_pakFiles = g_slist_append( g_pakFiles, file );
142
143                 vfsFixDOSName( filename_inzip );
144                  //-1 null terminated string
145                 filename_lower = g_ascii_strdown( filename_inzip, -1 );
146
147                 file->name = strdup( filename_lower );
148                 file->size = file_info.uncompressed_size;
149                 file->zipfile = uf;
150                 file->zippos = pos;
151
152                 if ( ( i + 1 ) < gi.number_entry ) {
153                         err = unzGoToNextFile( uf );
154                         if ( err != UNZ_OK ) {
155                                 break;
156                         }
157                 }
158                 g_free( filename_lower );
159         }
160 }
161
162 // =============================================================================
163 // Global functions
164
165 // reads all pak files from a dir
166 void vfsInitDirectory( const char *path ){
167         char filename[PATH_MAX];
168         char *dirlist;
169         GDir *dir;
170         int j;
171
172         for ( j = 0; j < g_numForbiddenDirs; ++j )
173         {
174                 char* dbuf = g_strdup( path );
175                 if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
176                         dbuf[strlen( dbuf ) - 1] = 0;
177                 }
178                 const char *p = strrchr( dbuf, '/' );
179                 p = ( p ? ( p + 1 ) : dbuf );
180                 if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
181                         g_free( dbuf );
182                         break;
183                 }
184                 g_free( dbuf );
185         }
186         if ( j < g_numForbiddenDirs ) {
187                 return;
188         }
189
190         if ( g_numDirs == VFS_MAXDIRS ) {
191                 Sys_FPrintf( SYS_WRN, "WARNING: too many VFS directories, can't init %s\n", path );
192                 return;
193         }
194
195         Sys_Printf( "VFS Init: %s\n", path );
196
197         strncpy( g_strDirs[g_numDirs], path, PATH_MAX );
198         g_strDirs[g_numDirs][PATH_MAX] = 0;
199         vfsFixDOSName( g_strDirs[g_numDirs] );
200         vfsAddSlash( g_strDirs[g_numDirs] );
201         g_numDirs++;
202
203         if ( g_bUsePak ) {
204                 dir = g_dir_open( path, 0, NULL );
205
206                 if ( dir != NULL ) {
207                         while ( 1 )
208                         {
209                                 const char* name = g_dir_read_name( dir );
210                                 if ( name == NULL ) {
211                                         break;
212                                 }
213
214                                 for ( j = 0; j < g_numForbiddenDirs; ++j )
215                                 {
216                                         const char *p = strrchr( name, '/' );
217                                         p = ( p ? ( p + 1 ) : name );
218                                         if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
219                                                 break;
220                                         }
221                                 }
222                                 if ( j < g_numForbiddenDirs ) {
223                                         continue;
224                                 }
225
226                                 dirlist = g_strdup( name );
227
228                                 {
229                                         char *ext = strrchr( dirlist, '.' );
230
231                                         if ( ext != NULL && ( !Q_stricmp( ext, ".pk3dir" ) || !Q_stricmp( ext, ".dpkdir" ) ) ) {
232                                                 if ( g_numDirs == VFS_MAXDIRS ) {
233                                                         g_free( dirlist );
234                                                         continue;
235                                                 }
236                                                 snprintf( g_strDirs[g_numDirs], PATH_MAX, "%s/%s", path, name );
237                                                 g_strDirs[g_numDirs][PATH_MAX-1] = '\0';
238                                                 vfsFixDOSName( g_strDirs[g_numDirs] );
239                                                 vfsAddSlash( g_strDirs[g_numDirs] );
240                                                 ++g_numDirs;
241                                         }
242
243                                         if ( ext == NULL || ( Q_stricmp( ext, ".pk3" ) != 0 && Q_stricmp( ext, ".dpk" ) != 0 ) ) {
244                                                 g_free( dirlist );
245                                                 continue;
246                                         }
247                                 }
248
249                                 sprintf( filename, "%s/%s", path, dirlist );
250                                 vfsInitPakFile( filename );
251
252                                 g_free( dirlist );
253                         }
254                         g_dir_close( dir );
255                 }
256         }
257 }
258
259 // frees all memory that we allocated
260 void vfsShutdown(){
261         while ( g_unzFiles )
262         {
263                 unzClose( (unzFile)g_unzFiles->data );
264                 g_unzFiles = g_slist_remove( g_unzFiles, g_unzFiles->data );
265         }
266
267         while ( g_pakFiles )
268         {
269                 VFS_PAKFILE* file = (VFS_PAKFILE*)g_pakFiles->data;
270                 free( file->name );
271                 free( file );
272                 g_pakFiles = g_slist_remove( g_pakFiles, file );
273         }
274 }
275
276 // return the number of files that match
277 int vfsGetFileCount( const char *filename ){
278         int i, count = 0;
279         char fixed[NAME_MAX], tmp[NAME_MAX];
280         char *lower;
281         GSList *lst;
282
283         strcpy( fixed, filename );
284         vfsFixDOSName( fixed );
285         lower = g_ascii_strdown( fixed, -1 );
286
287         for ( lst = g_pakFiles; lst != NULL; lst = g_slist_next( lst ) )
288         {
289                 VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
290
291                 if ( strcmp( file->name, lower ) == 0 ) {
292                         count++;
293                 }
294         }
295
296         for ( i = 0; i < g_numDirs; i++ )
297         {
298                 strcpy( tmp, g_strDirs[i] );
299                 strcat( tmp, lower );
300                 if ( access( tmp, R_OK ) == 0 ) {
301                         count++;
302                 }
303         }
304         g_free( lower );
305         return count;
306 }
307
308 static qboolean isSymlink(const unz_file_info64 *fileInfo) {
309         // see https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/stat.h
310         // redefine so it works outside of Unices
311         const unsigned long Q3MAP_S_IFMT = 00170000;
312         const unsigned long Q3MAP_S_IFLNK = 0120000;
313         // see https://trac.edgewall.org/attachment/ticket/8919/ZipDownload.patch
314         const unsigned long PKZIP_EXTERNAL_ATTR_FILE_TYPE_SHIFT = 16;
315
316         unsigned long attr = fileInfo->external_fa >> PKZIP_EXTERNAL_ATTR_FILE_TYPE_SHIFT;
317         return (attr & Q3MAP_S_IFMT) == Q3MAP_S_IFLNK;
318 }
319
320 // The zip format has a maximum filename size of 64K
321 static const int MAX_FILENAME_BUF = 65537;
322
323 /* The symlink implementation is ported from Dæmon engine implementation by slipher which was a complete rewrite of one illwieckz did on Dæmon by taking inspiration from Darkplaces engine.
324
325 See:
326
327 - https://github.com/DaemonEngine/Daemon/blob/master/src/common/FileSystem.cpp
328 - https://gitlab.com/xonotic/darkplaces/-/blob/div0-stable/fs.c
329
330 Some words by slipher:
331
332 > Symlinks are a bad feature which you should not use. Therefore, the implementation is as
333 > slow as possible with a full iteration of the archive performed for each symlink.
334
335 > The symlink path `relative` must be relative to the symlink's location.
336 > Only supports paths consisting of "../" 0 or more times, followed by non-magical path components.
337 */
338 void resolveSymlinkPath( const char* base, const char* relative, char* resolved ){
339
340         base = g_path_get_dirname( base );
341
342         while( g_str_has_prefix( relative, "../" ) )
343         {
344                 if ( base[0] == '\0' )
345                 {
346                         Sys_FPrintf( SYS_WRN, "Error while reading symbolic link: \"%s\": no such directory\n", base );
347                         resolved[0] = '\0';
348                         return;
349                 }
350
351                 base = g_path_get_dirname( base );
352                 relative += 3;
353         }
354
355         snprintf( resolved, MAX_FILENAME_BUF, "%s/%s", base, relative);
356 }
357
358 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
359 int vfsLoadFile( const char *filename, void **bufferptr, int index ){
360         int i, count = 0;
361         char tmp[NAME_MAX], fixed[NAME_MAX];
362         char *lower;
363         GSList *lst;
364
365         // filename is a full path
366         if ( index == -1 ) {
367                 long len;
368                 FILE *f;
369
370                 f = fopen( filename, "rb" );
371                 if ( f == NULL ) {
372                         return -1;
373                 }
374
375                 fseek( f, 0, SEEK_END );
376                 len = ftell( f );
377                 rewind( f );
378
379                 *bufferptr = safe_malloc( len + 1 );
380                 if ( *bufferptr == NULL ) {
381                         fclose( f );
382                         return -1;
383                 }
384
385                 if ( fread( *bufferptr, 1, len, f ) != (size_t) len ) {
386                         fclose( f );
387                         return -1;
388                 }
389                 fclose( f );
390
391                 // we need to end the buffer with a 0
392                 ( (char*) ( *bufferptr ) )[len] = 0;
393
394                 return len;
395         }
396
397         *bufferptr = NULL;
398         strncpy( fixed, filename, sizeof( fixed ) );
399         vfsFixDOSName( fixed );
400         lower = g_ascii_strdown( fixed, -1 );
401
402         for ( i = 0; i < g_numDirs; i++ )
403         {
404                 strcpy( tmp, g_strDirs[i] );
405                 strcat( tmp, filename );
406                 if ( access( tmp, R_OK ) == 0 ) {
407                         if ( count == index ) {
408                                 long len;
409                                 FILE *f;
410
411                                 f = fopen( tmp, "rb" );
412                                 if ( f == NULL ) {
413                                         return -1;
414                                 }
415
416                                 fseek( f, 0, SEEK_END );
417                                 len = ftell( f );
418                                 rewind( f );
419
420                                 *bufferptr = safe_malloc( len + 1 );
421                                 if ( *bufferptr == NULL ) {
422                                         fclose( f );
423                                         return -1;
424                                 }
425
426                                 if ( fread( *bufferptr, 1, len, f ) != (size_t) len ) {
427                                         fclose( f );
428                                         return -1;
429                                 }
430                                 fclose( f );
431
432                                 // we need to end the buffer with a 0
433                                 ( (char*) ( *bufferptr ) )[len] = 0;
434
435                                 return len;
436                         }
437
438                         count++;
439                 }
440         }
441
442         // Do not resolve more than 5 recursive symbolic links to
443         // prevent circular symbolic links.
444         int max_symlink_depth = 5;
445
446         openSymlinkTarget:
447         for ( lst = g_pakFiles; lst != NULL; lst = g_slist_next( lst ) )
448         {
449                 VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
450
451                 if ( strcmp( file->name, lower ) != 0 ) {
452                         continue;
453                 }
454
455                 if ( count == index ) {
456
457                 if ( unzGoToFilePos( file->zipfile, &file->zippos ) != UNZ_OK ) {
458                         return -1;
459                 }
460                         if ( unzOpenCurrentFile( file->zipfile ) != UNZ_OK ) {
461                                 return -1;
462                         }
463
464                         unz_file_info64 fileInfo;
465                         if ( unzGetCurrentFileInfo64( file->zipfile, &fileInfo, filename, sizeof(filename), NULL, 0, NULL, 0 ) != UNZ_OK ) {
466                                 return -1;
467                         }
468
469                         *bufferptr = safe_malloc( file->size + 1 );
470                         // we need to end the buffer with a 0
471                         ( (char*) ( *bufferptr ) )[file->size] = 0;
472
473                         i = unzReadCurrentFile( file->zipfile, *bufferptr, file->size );
474                         unzCloseCurrentFile( file->zipfile );
475
476                         if ( isSymlink( &fileInfo ) ) {
477                                 Sys_FPrintf( SYS_VRB, "Found symbolic link: \"%s\"\n", filename );
478
479                                 if ( max_symlink_depth == 0 ) {
480                                         Sys_FPrintf( SYS_WRN, "Maximum symbolic link depth reached\n" );
481                                         g_free( lower );
482                                         return -1;
483                                 }
484
485                                 max_symlink_depth--;
486
487                                 const char* relative = (const char*) *bufferptr;
488                                 char resolved[MAX_FILENAME_BUF];
489
490                                 resolveSymlinkPath( file->name, relative, resolved );
491
492                                 Sys_FPrintf( SYS_VRB, "Resolved symbolic link: \"%s\"\n", resolved );
493
494                                 g_free( lower );
495                                 strncpy( fixed, resolved, sizeof( fixed ) );
496                                 vfsFixDOSName( fixed );
497                                 lower = g_ascii_strdown( fixed, -1 );
498
499                                 // slow as possible full iteration of the archive
500                                 goto openSymlinkTarget;
501                         }
502
503                         if ( i < 0 ) {
504                                 g_free( lower );
505                                 return -1;
506                         }
507                         else{
508                                 g_free( lower );
509                                 return file->size;
510                         }
511                 }
512
513                 count++;
514         }
515
516         g_free( lower );
517         return -1;
518 }