]> de.git.xonotic.org Git - xonotic/netradiant.git/blobdiff - tools/quake3/common/vfs.c
q3map2: implement symlink resolution
[xonotic/netradiant.git] / tools / quake3 / common / vfs.c
index 04af830e1ee7d8c4917223a6ea416d0bdd4d2617..0bd6ed1e2f6284d8e9680c343dddd1a0f7ae1eaa 100644 (file)
 // Leonardo Zide (leo@lokigames.com)
 //
 
-#include <stdio.h>
-
-#if defined ( __linux__ ) || defined ( __APPLE__ )
-#include <dirent.h>
-#include <unistd.h>
-#else
-#include <wtypes.h>
-#include <io.h>
-#define R_OK 04
-#define S_ISDIR( mode ) ( mode & _S_IFDIR )
-#define PATH_MAX 260
-#endif
-
 #include <string.h>
 #include <stdlib.h>
 #include <sys/stat.h>
 
 #include "cmdlib.h"
+#include "filematch.h"
 #include "mathlib.h"
-#include <glib.h>
 #include "inout.h"
 #include "vfs.h"
-#include "unzip.h"
+#include <unzip.h>
+#include <glib.h>
 
 typedef struct
 {
        char*   name;
-       unz_s zipinfo;
        unzFile zipfile;
+       unz_file_pos zippos;
        guint32 size;
 } VFS_PAKFILE;
 
@@ -78,8 +66,10 @@ typedef struct
 
 static GSList*  g_unzFiles;
 static GSList*  g_pakFiles;
-static char g_strDirs[VFS_MAXDIRS][PATH_MAX];
+static char g_strDirs[VFS_MAXDIRS][PATH_MAX + 1];
 static int g_numDirs;
+char g_strForbiddenDirs[VFS_MAXDIRS][PATH_MAX + 1];
+int g_numForbiddenDirs = 0;
 static gboolean g_bUsePak = TRUE;
 
 // =============================================================================
@@ -133,6 +123,7 @@ static void vfsInitPakFile( const char *filename ){
        for ( i = 0; i < gi.number_entry; i++ )
        {
                char filename_inzip[NAME_MAX];
+               char *filename_lower;
                unz_file_info file_info;
                VFS_PAKFILE* file;
 
@@ -140,17 +131,23 @@ static void vfsInitPakFile( const char *filename ){
                if ( err != UNZ_OK ) {
                        break;
                }
+               unz_file_pos pos;
+               err = unzGetFilePos( uf, &pos );
+               if ( err != UNZ_OK ) {
+                       break;
+               }
 
                file = (VFS_PAKFILE*)safe_malloc( sizeof( VFS_PAKFILE ) );
                g_pakFiles = g_slist_append( g_pakFiles, file );
 
                vfsFixDOSName( filename_inzip );
-               g_strdown( filename_inzip );
+                //-1 null terminated string
+               filename_lower = g_ascii_strdown( filename_inzip, -1 );
 
-               file->name = strdup( filename_inzip );
+               file->name = strdup( filename_lower );
                file->size = file_info.uncompressed_size;
                file->zipfile = uf;
-               memcpy( &file->zipinfo, uf, sizeof( unz_s ) );
+               file->zippos = pos;
 
                if ( ( i + 1 ) < gi.number_entry ) {
                        err = unzGoToNextFile( uf );
@@ -158,6 +155,7 @@ static void vfsInitPakFile( const char *filename ){
                                break;
                        }
                }
+               g_free( filename_lower );
        }
 }
 
@@ -169,14 +167,34 @@ void vfsInitDirectory( const char *path ){
        char filename[PATH_MAX];
        char *dirlist;
        GDir *dir;
+       int j;
+
+       for ( j = 0; j < g_numForbiddenDirs; ++j )
+       {
+               char* dbuf = g_strdup( path );
+               if ( *dbuf && dbuf[strlen( dbuf ) - 1] == '/' ) {
+                       dbuf[strlen( dbuf ) - 1] = 0;
+               }
+               const char *p = strrchr( dbuf, '/' );
+               p = ( p ? ( p + 1 ) : dbuf );
+               if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
+                       g_free( dbuf );
+                       break;
+               }
+               g_free( dbuf );
+       }
+       if ( j < g_numForbiddenDirs ) {
+               return;
+       }
 
-       if ( g_numDirs == ( VFS_MAXDIRS - 1 ) ) {
+       if ( g_numDirs == VFS_MAXDIRS ) {
                return;
        }
 
        Sys_Printf( "VFS Init: %s\n", path );
 
-       strcpy( g_strDirs[g_numDirs], path );
+       strncpy( g_strDirs[g_numDirs], path, PATH_MAX );
+       g_strDirs[g_numDirs][PATH_MAX] = 0;
        vfsFixDOSName( g_strDirs[g_numDirs] );
        vfsAddSlash( g_strDirs[g_numDirs] );
        g_numDirs++;
@@ -192,11 +210,37 @@ void vfsInitDirectory( const char *path ){
                                        break;
                                }
 
+                               for ( j = 0; j < g_numForbiddenDirs; ++j )
+                               {
+                                       const char *p = strrchr( name, '/' );
+                                       p = ( p ? ( p + 1 ) : name );
+                                       if ( matchpattern( p, g_strForbiddenDirs[j], TRUE ) ) {
+                                               break;
+                                       }
+                               }
+                               if ( j < g_numForbiddenDirs ) {
+                                       continue;
+                               }
+
                                dirlist = g_strdup( name );
 
                                {
                                        char *ext = strrchr( dirlist, '.' );
-                                       if ( ( ext == NULL ) || ( Q_stricmp( ext, ".pk3" ) != 0 ) ) {
+
+                                       if ( ext != NULL && ( !Q_stricmp( ext, ".pk3dir" ) || !Q_stricmp( ext, ".dpkdir" ) ) ) {
+                                               if ( g_numDirs == VFS_MAXDIRS ) {
+                                                       g_free( dirlist );
+                                                       continue;
+                                               }
+                                               snprintf( g_strDirs[g_numDirs], PATH_MAX, "%s/%s", path, name );
+                                               g_strDirs[g_numDirs][PATH_MAX-1] = '\0';
+                                               vfsFixDOSName( g_strDirs[g_numDirs] );
+                                               vfsAddSlash( g_strDirs[g_numDirs] );
+                                               ++g_numDirs;
+                                       }
+
+                                       if ( ext == NULL || ( Q_stricmp( ext, ".pk3" ) != 0 && Q_stricmp( ext, ".dpk" ) != 0 ) ) {
+                                               g_free( dirlist );
                                                continue;
                                        }
                                }
@@ -232,17 +276,18 @@ void vfsShutdown(){
 int vfsGetFileCount( const char *filename ){
        int i, count = 0;
        char fixed[NAME_MAX], tmp[NAME_MAX];
+       char *lower;
        GSList *lst;
 
        strcpy( fixed, filename );
        vfsFixDOSName( fixed );
-       g_strdown( fixed );
+       lower = g_ascii_strdown( fixed, -1 );
 
        for ( lst = g_pakFiles; lst != NULL; lst = g_slist_next( lst ) )
        {
                VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
 
-               if ( strcmp( file->name, fixed ) == 0 ) {
+               if ( strcmp( file->name, lower ) == 0 ) {
                        count++;
                }
        }
@@ -250,19 +295,70 @@ int vfsGetFileCount( const char *filename ){
        for ( i = 0; i < g_numDirs; i++ )
        {
                strcpy( tmp, g_strDirs[i] );
-               strcat( tmp, fixed );
+               strcat( tmp, lower );
                if ( access( tmp, R_OK ) == 0 ) {
                        count++;
                }
        }
-
+       g_free( lower );
        return count;
 }
 
+static qboolean isSymlink(const unz_file_info64 *fileInfo) {
+       // see https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/stat.h
+       // redefine so it works outside of Unices
+       const unsigned long Q3MAP_S_IFMT = 00170000;
+       const unsigned long Q3MAP_S_IFLNK = 0120000;
+       // see https://trac.edgewall.org/attachment/ticket/8919/ZipDownload.patch
+       const unsigned long PKZIP_EXTERNAL_ATTR_FILE_TYPE_SHIFT = 16;
+
+       unsigned long attr = fileInfo->external_fa >> PKZIP_EXTERNAL_ATTR_FILE_TYPE_SHIFT;
+       return (attr & Q3MAP_S_IFMT) == Q3MAP_S_IFLNK;
+}
+
+// The zip format has a maximum filename size of 64K
+static const int MAX_FILENAME_BUF = 65537;
+
+/* 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.
+
+See:
+
+- https://github.com/DaemonEngine/Daemon/blob/master/src/common/FileSystem.cpp
+- https://gitlab.com/xonotic/darkplaces/-/blob/div0-stable/fs.c
+
+Some words by slipher:
+
+> Symlinks are a bad feature which you should not use. Therefore, the implementation is as
+> slow as possible with a full iteration of the archive performed for each symlink.
+
+> The symlink path `relative` must be relative to the symlink's location.
+> Only supports paths consisting of "../" 0 or more times, followed by non-magical path components.
+*/
+void resolveSymlinkPath( const char* base, const char* relative, char* resolved ){
+
+       base = g_path_get_dirname( base );
+
+       while( g_str_has_prefix( relative, "../" ) )
+       {
+               if ( base[0] == '\0' )
+               {
+                       Sys_FPrintf( SYS_WRN, "Error while reading symbolic link: \"%s\": no such directory\n", base );
+                       resolved[0] = '\0';
+                       return;
+               }
+
+               base = g_path_get_dirname( base );
+               relative += 3;
+       }
+
+       snprintf( resolved, MAX_FILENAME_BUF, "%s/%s", base, relative);
+}
+
 // NOTE: when loading a file, you have to allocate one extra byte and set it to \0
 int vfsLoadFile( const char *filename, void **bufferptr, int index ){
        int i, count = 0;
        char tmp[NAME_MAX], fixed[NAME_MAX];
+       char *lower;
        GSList *lst;
 
        // filename is a full path
@@ -281,10 +377,14 @@ int vfsLoadFile( const char *filename, void **bufferptr, int index ){
 
                *bufferptr = safe_malloc( len + 1 );
                if ( *bufferptr == NULL ) {
+                       fclose( f );
                        return -1;
                }
 
-               fread( *bufferptr, 1, len, f );
+               if ( fread( *bufferptr, 1, len, f ) != (size_t) len ) {
+                       fclose( f );
+                       return -1;
+               }
                fclose( f );
 
                // we need to end the buffer with a 0
@@ -294,9 +394,9 @@ int vfsLoadFile( const char *filename, void **bufferptr, int index ){
        }
 
        *bufferptr = NULL;
-       strcpy( fixed, filename );
+       strncpy( fixed, filename, sizeof( fixed ) );
        vfsFixDOSName( fixed );
-       g_strdown( fixed );
+       lower = g_ascii_strdown( fixed, -1 );
 
        for ( i = 0; i < g_numDirs; i++ )
        {
@@ -318,10 +418,14 @@ int vfsLoadFile( const char *filename, void **bufferptr, int index ){
 
                                *bufferptr = safe_malloc( len + 1 );
                                if ( *bufferptr == NULL ) {
+                                       fclose( f );
                                        return -1;
                                }
 
-                               fread( *bufferptr, 1, len, f );
+                               if ( fread( *bufferptr, 1, len, f ) != (size_t) len ) {
+                                       fclose( f );
+                                       return -1;
+                               }
                                fclose( f );
 
                                // we need to end the buffer with a 0
@@ -334,31 +438,73 @@ int vfsLoadFile( const char *filename, void **bufferptr, int index ){
                }
        }
 
+       // Do not resolve more than 5 recursive symbolic links to
+       // prevent circular symbolic links.
+       int max_symlink_depth = 5;
+
+       openSymlinkTarget:
        for ( lst = g_pakFiles; lst != NULL; lst = g_slist_next( lst ) )
        {
                VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;
 
-               if ( strcmp( file->name, fixed ) != 0 ) {
+               if ( strcmp( file->name, lower ) != 0 ) {
                        continue;
                }
 
                if ( count == index ) {
-                       memcpy( file->zipfile, &file->zipinfo, sizeof( unz_s ) );
 
+               if ( unzGoToFilePos( file->zipfile, &file->zippos ) != UNZ_OK ) {
+                       return -1;
+               }
                        if ( unzOpenCurrentFile( file->zipfile ) != UNZ_OK ) {
                                return -1;
                        }
 
+                       unz_file_info64 fileInfo;
+                       if ( unzGetCurrentFileInfo64( file->zipfile, &fileInfo, filename, sizeof(filename), NULL, 0, NULL, 0 ) != UNZ_OK ) {
+                               return -1;
+                       }
+
                        *bufferptr = safe_malloc( file->size + 1 );
                        // we need to end the buffer with a 0
                        ( (char*) ( *bufferptr ) )[file->size] = 0;
 
                        i = unzReadCurrentFile( file->zipfile, *bufferptr, file->size );
                        unzCloseCurrentFile( file->zipfile );
+
+                       if ( isSymlink( &fileInfo ) ) {
+                               Sys_FPrintf( SYS_VRB, "Found symbolic link: \"%s\"\n", filename );
+
+                               if ( max_symlink_depth == 0 ) {
+                                       Sys_FPrintf( SYS_WRN, "Maximum symbolic link depth reached\n" );
+                                       g_free( lower );
+                                       return -1;
+                               }
+
+                               max_symlink_depth--;
+
+                               const char* relative = (const char*) *bufferptr;
+                               char resolved[MAX_FILENAME_BUF];
+
+                               resolveSymlinkPath( file->name, relative, resolved );
+
+                               Sys_FPrintf( SYS_VRB, "Resolved symbolic link: \"%s\"\n", resolved );
+
+                               g_free( lower );
+                               strncpy( fixed, resolved, sizeof( fixed ) );
+                               vfsFixDOSName( fixed );
+                               lower = g_ascii_strdown( fixed, -1 );
+
+                               // slow as possible full iteration of the archive
+                               goto openSymlinkTarget;
+                       }
+
                        if ( i < 0 ) {
+                               g_free( lower );
                                return -1;
                        }
                        else{
+                               g_free( lower );
                                return file->size;
                        }
                }
@@ -366,5 +512,6 @@ int vfsLoadFile( const char *filename, void **bufferptr, int index ){
                count++;
        }
 
+       g_free( lower );
        return -1;
 }