0bd6ed1e2f6284d8e9680c343dddd1a0f7ae1eaa
[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                 return;
192         }
193
194         Sys_Printf( "VFS Init: %s\n", path );
195
196         strncpy( g_strDirs[g_numDirs], path, PATH_MAX );
197         g_strDirs[g_numDirs][PATH_MAX] = 0;
198         vfsFixDOSName( g_strDirs[g_numDirs] );
199         vfsAddSlash( g_strDirs[g_numDirs] );
200         g_numDirs++;
201
202         if ( g_bUsePak ) {
203                 dir = g_dir_open( path, 0, NULL );
204
205                 if ( dir != NULL ) {
206                         while ( 1 )
207                         {
208                                 const char* name = g_dir_read_name( dir );
209                                 if ( name == NULL ) {
210                                         break;
211                                 }
212
213                                 for ( j = 0; j < g_numForbiddenDirs; ++j )
214                                 {
215                                         const char *p = strrchr( name, '/' );
216                                         p = ( p ? ( p + 1 ) : name );
217                                         if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
218                                                 break;
219                                         }
220                                 }
221                                 if ( j < g_numForbiddenDirs ) {
222                                         continue;
223                                 }
224
225                                 dirlist = g_strdup( name );
226
227                                 {
228                                         char *ext = strrchr( dirlist, '.' );
229
230                                         if ( ext != NULL && ( !Q_stricmp( ext, ".pk3dir" ) || !Q_stricmp( ext, ".dpkdir" ) ) ) {
231                                                 if ( g_numDirs == VFS_MAXDIRS ) {
232                                                         g_free( dirlist );
233                                                         continue;
234                                                 }
235                                                 snprintf( g_strDirs[g_numDirs], PATH_MAX, "%s/%s", path, name );
236                                                 g_strDirs[g_numDirs][PATH_MAX-1] = '\0';
237                                                 vfsFixDOSName( g_strDirs[g_numDirs] );
238                                                 vfsAddSlash( g_strDirs[g_numDirs] );
239                                                 ++g_numDirs;
240                                         }
241
242                                         if ( ext == NULL || ( Q_stricmp( ext, ".pk3" ) != 0 && Q_stricmp( ext, ".dpk" ) != 0 ) ) {
243                                                 g_free( dirlist );
244                                                 continue;
245                                         }
246                                 }
247
248                                 sprintf( filename, "%s/%s", path, dirlist );
249                                 vfsInitPakFile( filename );
250
251                                 g_free( dirlist );
252                         }
253                         g_dir_close( dir );
254                 }
255         }
256 }
257
258 // frees all memory that we allocated
259 void vfsShutdown(){
260         while ( g_unzFiles )
261         {
262                 unzClose( (unzFile)g_unzFiles->data );
263                 g_unzFiles = g_slist_remove( g_unzFiles, g_unzFiles->data );
264         }
265
266         while ( g_pakFiles )
267         {
268                 VFS_PAKFILE* file = (VFS_PAKFILE*)g_pakFiles->data;
269                 free( file->name );
270                 free( file );
271                 g_pakFiles = g_slist_remove( g_pakFiles, file );
272         }
273 }
274
275 // return the number of files that match
276 int vfsGetFileCount( const char *filename ){
277         int i, count = 0;
278         char fixed[NAME_MAX], tmp[NAME_MAX];
279         char *lower;
280         GSList *lst;
281
282         strcpy( fixed, filename );
283         vfsFixDOSName( fixed );
284         lower = g_ascii_strdown( fixed, -1 );
285
286         for ( lst = g_pakFiles; lst != NULL; lst = g_slist_next( lst ) )
287         {
288                 VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
289
290                 if ( strcmp( file->name, lower ) == 0 ) {
291                         count++;
292                 }
293         }
294
295         for ( i = 0; i < g_numDirs; i++ )
296         {
297                 strcpy( tmp, g_strDirs[i] );
298                 strcat( tmp, lower );
299                 if ( access( tmp, R_OK ) == 0 ) {
300                         count++;
301                 }
302         }
303         g_free( lower );
304         return count;
305 }
306
307 static qboolean isSymlink(const unz_file_info64 *fileInfo) {
308         // see https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/stat.h
309         // redefine so it works outside of Unices
310         const unsigned long Q3MAP_S_IFMT = 00170000;
311         const unsigned long Q3MAP_S_IFLNK = 0120000;
312         // see https://trac.edgewall.org/attachment/ticket/8919/ZipDownload.patch
313         const unsigned long PKZIP_EXTERNAL_ATTR_FILE_TYPE_SHIFT = 16;
314
315         unsigned long attr = fileInfo->external_fa >> PKZIP_EXTERNAL_ATTR_FILE_TYPE_SHIFT;
316         return (attr & Q3MAP_S_IFMT) == Q3MAP_S_IFLNK;
317 }
318
319 // The zip format has a maximum filename size of 64K
320 static const int MAX_FILENAME_BUF = 65537;
321
322 /* 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.
323
324 See:
325
326 - https://github.com/DaemonEngine/Daemon/blob/master/src/common/FileSystem.cpp
327 - https://gitlab.com/xonotic/darkplaces/-/blob/div0-stable/fs.c
328
329 Some words by slipher:
330
331 > Symlinks are a bad feature which you should not use. Therefore, the implementation is as
332 > slow as possible with a full iteration of the archive performed for each symlink.
333
334 > The symlink path `relative` must be relative to the symlink's location.
335 > Only supports paths consisting of "../" 0 or more times, followed by non-magical path components.
336 */
337 void resolveSymlinkPath( const char* base, const char* relative, char* resolved ){
338
339         base = g_path_get_dirname( base );
340
341         while( g_str_has_prefix( relative, "../" ) )
342         {
343                 if ( base[0] == '\0' )
344                 {
345                         Sys_FPrintf( SYS_WRN, "Error while reading symbolic link: \"%s\": no such directory\n", base );
346                         resolved[0] = '\0';
347                         return;
348                 }
349
350                 base = g_path_get_dirname( base );
351                 relative += 3;
352         }
353
354         snprintf( resolved, MAX_FILENAME_BUF, "%s/%s", base, relative);
355 }
356
357 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
358 int vfsLoadFile( const char *filename, void **bufferptr, int index ){
359         int i, count = 0;
360         char tmp[NAME_MAX], fixed[NAME_MAX];
361         char *lower;
362         GSList *lst;
363
364         // filename is a full path
365         if ( index == -1 ) {
366                 long len;
367                 FILE *f;
368
369                 f = fopen( filename, "rb" );
370                 if ( f == NULL ) {
371                         return -1;
372                 }
373
374                 fseek( f, 0, SEEK_END );
375                 len = ftell( f );
376                 rewind( f );
377
378                 *bufferptr = safe_malloc( len + 1 );
379                 if ( *bufferptr == NULL ) {
380                         fclose( f );
381                         return -1;
382                 }
383
384                 if ( fread( *bufferptr, 1, len, f ) != (size_t) len ) {
385                         fclose( f );
386                         return -1;
387                 }
388                 fclose( f );
389
390                 // we need to end the buffer with a 0
391                 ( (char*) ( *bufferptr ) )[len] = 0;
392
393                 return len;
394         }
395
396         *bufferptr = NULL;
397         strncpy( fixed, filename, sizeof( fixed ) );
398         vfsFixDOSName( fixed );
399         lower = g_ascii_strdown( fixed, -1 );
400
401         for ( i = 0; i < g_numDirs; i++ )
402         {
403                 strcpy( tmp, g_strDirs[i] );
404                 strcat( tmp, filename );
405                 if ( access( tmp, R_OK ) == 0 ) {
406                         if ( count == index ) {
407                                 long len;
408                                 FILE *f;
409
410                                 f = fopen( tmp, "rb" );
411                                 if ( f == NULL ) {
412                                         return -1;
413                                 }
414
415                                 fseek( f, 0, SEEK_END );
416                                 len = ftell( f );
417                                 rewind( f );
418
419                                 *bufferptr = safe_malloc( len + 1 );
420                                 if ( *bufferptr == NULL ) {
421                                         fclose( f );
422                                         return -1;
423                                 }
424
425                                 if ( fread( *bufferptr, 1, len, f ) != (size_t) len ) {
426                                         fclose( f );
427                                         return -1;
428                                 }
429                                 fclose( f );
430
431                                 // we need to end the buffer with a 0
432                                 ( (char*) ( *bufferptr ) )[len] = 0;
433
434                                 return len;
435                         }
436
437                         count++;
438                 }
439         }
440
441         // Do not resolve more than 5 recursive symbolic links to
442         // prevent circular symbolic links.
443         int max_symlink_depth = 5;
444
445         openSymlinkTarget:
446         for ( lst = g_pakFiles; lst != NULL; lst = g_slist_next( lst ) )
447         {
448                 VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
449
450                 if ( strcmp( file->name, lower ) != 0 ) {
451                         continue;
452                 }
453
454                 if ( count == index ) {
455
456                 if ( unzGoToFilePos( file->zipfile, &file->zippos ) != UNZ_OK ) {
457                         return -1;
458                 }
459                         if ( unzOpenCurrentFile( file->zipfile ) != UNZ_OK ) {
460                                 return -1;
461                         }
462
463                         unz_file_info64 fileInfo;
464                         if ( unzGetCurrentFileInfo64( file->zipfile, &fileInfo, filename, sizeof(filename), NULL, 0, NULL, 0 ) != UNZ_OK ) {
465                                 return -1;
466                         }
467
468                         *bufferptr = safe_malloc( file->size + 1 );
469                         // we need to end the buffer with a 0
470                         ( (char*) ( *bufferptr ) )[file->size] = 0;
471
472                         i = unzReadCurrentFile( file->zipfile, *bufferptr, file->size );
473                         unzCloseCurrentFile( file->zipfile );
474
475                         if ( isSymlink( &fileInfo ) ) {
476                                 Sys_FPrintf( SYS_VRB, "Found symbolic link: \"%s\"\n", filename );
477
478                                 if ( max_symlink_depth == 0 ) {
479                                         Sys_FPrintf( SYS_WRN, "Maximum symbolic link depth reached\n" );
480                                         g_free( lower );
481                                         return -1;
482                                 }
483
484                                 max_symlink_depth--;
485
486                                 const char* relative = (const char*) *bufferptr;
487                                 char resolved[MAX_FILENAME_BUF];
488
489                                 resolveSymlinkPath( file->name, relative, resolved );
490
491                                 Sys_FPrintf( SYS_VRB, "Resolved symbolic link: \"%s\"\n", resolved );
492
493                                 g_free( lower );
494                                 strncpy( fixed, resolved, sizeof( fixed ) );
495                                 vfsFixDOSName( fixed );
496                                 lower = g_ascii_strdown( fixed, -1 );
497
498                                 // slow as possible full iteration of the archive
499                                 goto openSymlinkTarget;
500                         }
501
502                         if ( i < 0 ) {
503                                 g_free( lower );
504                                 return -1;
505                         }
506                         else{
507                                 g_free( lower );
508                                 return file->size;
509                         }
510                 }
511
512                 count++;
513         }
514
515         g_free( lower );
516         return -1;
517 }