]> 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 2d2ab13810a37880f4e8a071d98eba837e4d56a0..0bd6ed1e2f6284d8e9680c343dddd1a0f7ae1eaa 100644 (file)
-/*\r
-Copyright (c) 2001, Loki software, inc.\r
-All rights reserved.\r
-\r
-Redistribution and use in source and binary forms, with or without modification, \r
-are permitted provided that the following conditions are met:\r
-  \r
-Redistributions of source code must retain the above copyright notice, this list \r
-of conditions and the following disclaimer.\r
-    \r
-Redistributions in binary form must reproduce the above copyright notice, this\r
-list of conditions and the following disclaimer in the documentation and/or\r
-other materials provided with the distribution.\r
-      \r
-Neither the name of Loki software nor the names of its contributors may be used \r
-to endorse or promote products derived from this software without specific prior \r
-written permission. \r
-        \r
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' \r
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE \r
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE \r
-DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY \r
-DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES \r
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; \r
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON \r
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT \r
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS \r
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \r
-*/\r
-\r
-//\r
-// Rules:\r
-//\r
-// - Directories should be searched in the following order: ~/.q3a/baseq3,\r
-//   install dir (/usr/local/games/quake3/baseq3) and cd_path (/mnt/cdrom/baseq3).\r
-//\r
-// - Pak files are searched first inside the directories.\r
-// - Case insensitive.\r
-// - Unix-style slashes (/) (windows is backwards .. everyone knows that)\r
-//\r
-// Leonardo Zide (leo@lokigames.com)\r
-//\r
-\r
-#include <stdio.h>\r
-\r
-#if defined (__linux__) || defined (__APPLE__)\r
-#include <dirent.h>\r
-#include <unistd.h>\r
-#else\r
-#include <wtypes.h>\r
-#include <io.h>\r
-#define R_OK 04\r
-#define S_ISDIR(mode) (mode & _S_IFDIR)\r
-#define PATH_MAX 260\r
-#endif\r
-\r
-#include <string.h>\r
-#include <stdlib.h>\r
-#include <sys/stat.h>\r
-\r
-#include "cmdlib.h"\r
-#include "mathlib.h"\r
-#include "glib.h"\r
-#include "inout.h"\r
-#include "vfs.h"\r
-#include "unzip.h"\r
-\r
-typedef struct\r
-{\r
-  char*   name;\r
-  unz_s   zipinfo;\r
-  unzFile zipfile;\r
-  guint32   size;\r
-} VFS_PAKFILE;\r
-\r
-// =============================================================================\r
-// Global variables\r
-\r
-static GSList*  g_unzFiles;\r
-static GSList*  g_pakFiles;\r
-static char     g_strDirs[VFS_MAXDIRS][PATH_MAX];\r
-static int      g_numDirs;\r
-static gboolean g_bUsePak = TRUE;\r
-\r
-// =============================================================================\r
-// Static functions\r
-\r
-static void vfsAddSlash (char *str)\r
-{\r
-  int n = strlen (str);\r
-  if (n > 0)\r
-  {\r
-    if (str[n-1] != '\\' && str[n-1] != '/')\r
-      strcat (str, "/");\r
-  }\r
-}\r
-\r
-static void vfsFixDOSName (char *src)\r
-{\r
-  if (src == NULL)\r
-    return;\r
-  \r
-  while (*src)\r
-  {\r
-    if (*src == '\\')\r
-      *src = '/';\r
-    src++;\r
-  }\r
-}\r
-\r
-//!\todo Define globally or use heap-allocated string.\r
-#define NAME_MAX 255\r
-\r
-static void vfsInitPakFile (const char *filename)\r
-{\r
-  unz_global_info gi;\r
-  unzFile uf;\r
-  guint32 i;\r
-  int err;\r
-  \r
-  uf = unzOpen (filename);\r
-  if (uf == NULL)\r
-    return;\r
-  \r
-  g_unzFiles = g_slist_append (g_unzFiles, uf);\r
-  \r
-  err = unzGetGlobalInfo (uf,&gi);\r
-  if (err != UNZ_OK)\r
-    return;\r
-  unzGoToFirstFile(uf);\r
-  \r
-  for (i = 0; i < gi.number_entry; i++)\r
-  {\r
-    char filename_inzip[NAME_MAX];\r
-    unz_file_info file_info;\r
-    VFS_PAKFILE* file;\r
-    \r
-    err = unzGetCurrentFileInfo (uf, &file_info, filename_inzip, sizeof(filename_inzip), NULL, 0, NULL, 0);\r
-    if (err != UNZ_OK)\r
-      break;\r
-    \r
-    file = (VFS_PAKFILE*)safe_malloc (sizeof (VFS_PAKFILE));\r
-    g_pakFiles = g_slist_append (g_pakFiles, file);\r
-    \r
-    vfsFixDOSName (filename_inzip);\r
-    g_strdown (filename_inzip);\r
-    \r
-    file->name = strdup (filename_inzip);\r
-    file->size = file_info.uncompressed_size;\r
-    file->zipfile = uf;\r
-    memcpy (&file->zipinfo, uf, sizeof (unz_s));\r
-    \r
-    if ((i+1) < gi.number_entry)\r
-    {\r
-      err = unzGoToNextFile(uf);\r
-      if (err!=UNZ_OK)\r
-        break;\r
-    }\r
-  }\r
-}\r
-\r
-// =============================================================================\r
-// Global functions\r
-\r
-// reads all pak files from a dir\r
-void vfsInitDirectory (const char *path)\r
-{\r
-  char filename[PATH_MAX];\r
-  char *dirlist;\r
-  GDir *dir;\r
-  \r
-  if (g_numDirs == (VFS_MAXDIRS-1))\r
-    return;\r
-  \r
-  Sys_Printf ("VFS Init: %s\n", path);\r
-  \r
-  strcpy (g_strDirs[g_numDirs], path);\r
-  vfsFixDOSName (g_strDirs[g_numDirs]);\r
-  vfsAddSlash (g_strDirs[g_numDirs]);\r
-  g_numDirs++;\r
-  \r
-  if (g_bUsePak)\r
-  {\r
-    dir = g_dir_open (path, 0, NULL);\r
-\r
-    if (dir != NULL)\r
-    {\r
-      while (1)\r
-      {\r
-        const char* name = g_dir_read_name(dir);\r
-        if(name == NULL)\r
-          break;\r
-\r
-        dirlist = g_strdup(name);\r
-\r
-        {\r
-          char *ext = strrchr (dirlist, '.');\r
-          if ((ext == NULL) || (Q_stricmp (ext, ".pk3") != 0))\r
-            continue;\r
-        }\r
-        \r
-        sprintf (filename, "%s/%s", path, dirlist);\r
-        vfsInitPakFile (filename);\r
-\r
-        g_free(dirlist);\r
-      }\r
-      g_dir_close (dir);\r
-    }\r
-  }\r
-}\r
-\r
-// frees all memory that we allocated\r
-void vfsShutdown ()\r
-{\r
-  while (g_unzFiles)\r
-  {\r
-    unzClose ((unzFile)g_unzFiles->data);\r
-    g_unzFiles = g_slist_remove (g_unzFiles, g_unzFiles->data);\r
-  }\r
-  \r
-  while (g_pakFiles)\r
-  {\r
-    VFS_PAKFILE* file = (VFS_PAKFILE*)g_pakFiles->data;\r
-    free (file->name);\r
-    free (file);\r
-    g_pakFiles = g_slist_remove (g_pakFiles, file);\r
-  }\r
-}\r
-\r
-// return the number of files that match\r
-int vfsGetFileCount (const char *filename)\r
-{\r
-  int i, count = 0;\r
-  char fixed[NAME_MAX], tmp[NAME_MAX];\r
-  GSList *lst;\r
-  \r
-  strcpy (fixed, filename);\r
-  vfsFixDOSName (fixed);\r
-  g_strdown (fixed);\r
-  \r
-  for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))\r
-  {\r
-    VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;\r
-    \r
-    if (strcmp (file->name, fixed) == 0)\r
-      count++;\r
-  }\r
-  \r
-  for (i = 0; i < g_numDirs; i++)\r
-  {\r
-    strcpy (tmp, g_strDirs[i]);\r
-    strcat (tmp, fixed);\r
-    if (access (tmp, R_OK) == 0)\r
-      count++;\r
-  }\r
-  \r
-  return count;\r
-}\r
-\r
-// NOTE: when loading a file, you have to allocate one extra byte and set it to \0\r
-int vfsLoadFile (const char *filename, void **bufferptr, int index)\r
-{\r
-  int i, count = 0;\r
-  char tmp[NAME_MAX], fixed[NAME_MAX];\r
-  GSList *lst;\r
-  \r
-  // filename is a full path\r
-  if (index == -1)\r
-  {\r
-    long len;\r
-    FILE *f;\r
-    \r
-    f = fopen (filename, "rb");\r
-    if (f == NULL)\r
-      return -1;\r
-    \r
-    fseek (f, 0, SEEK_END);\r
-    len = ftell (f);\r
-    rewind (f);\r
-    \r
-    *bufferptr = safe_malloc (len+1);\r
-    if (*bufferptr == NULL)\r
-      return -1;\r
-    \r
-    fread (*bufferptr, 1, len, f);\r
-    fclose (f);\r
-    \r
-    // we need to end the buffer with a 0\r
-    ((char*) (*bufferptr))[len] = 0;\r
-    \r
-    return len;\r
-  }\r
-  \r
-  *bufferptr = NULL;\r
-  strcpy (fixed, filename);\r
-  vfsFixDOSName (fixed);\r
-  g_strdown (fixed);\r
-  \r
-  for (i = 0; i < g_numDirs; i++)\r
-  {\r
-    strcpy (tmp, g_strDirs[i]);\r
-    strcat (tmp, filename);\r
-    if (access (tmp, R_OK) == 0)\r
-    {\r
-      if (count == index)\r
-      {\r
-        long len;\r
-        FILE *f;\r
-        \r
-        f = fopen (tmp, "rb");\r
-        if (f == NULL)\r
-          return -1;\r
-        \r
-        fseek (f, 0, SEEK_END);\r
-        len = ftell (f);\r
-        rewind (f);\r
-        \r
-        *bufferptr = safe_malloc (len+1);\r
-        if (*bufferptr == NULL)\r
-          return -1;\r
-        \r
-        fread (*bufferptr, 1, len, f);\r
-        fclose (f);\r
-        \r
-        // we need to end the buffer with a 0\r
-        ((char*) (*bufferptr))[len] = 0;\r
-        \r
-        return len;\r
-      }\r
-      \r
-      count++;\r
-    }\r
-  }\r
-  \r
-  for (lst = g_pakFiles; lst != NULL; lst = g_slist_next (lst))\r
-  {\r
-    VFS_PAKFILE* file = (VFS_PAKFILE*)lst->data;\r
-    \r
-    if (strcmp (file->name, fixed) != 0)\r
-      continue;\r
-    \r
-    if (count == index)\r
-    {\r
-      memcpy (file->zipfile, &file->zipinfo, sizeof (unz_s));\r
-      \r
-      if (unzOpenCurrentFile (file->zipfile) != UNZ_OK)\r
-        return -1;\r
-      \r
-      *bufferptr = safe_malloc (file->size+1);\r
-      // we need to end the buffer with a 0\r
-      ((char*) (*bufferptr))[file->size] = 0;\r
-      \r
-      i = unzReadCurrentFile (file->zipfile , *bufferptr, file->size);\r
-      unzCloseCurrentFile (file->zipfile);\r
-      if (i < 0)\r
-        return -1;\r
-      else\r
-        return file->size;\r
-    }\r
-    \r
-    count++;\r
-  }\r
-  \r
-  return -1;\r
-}\r
+/*
+   Copyright (c) 2001, Loki software, inc.
+   All rights reserved.
+
+   Redistribution and use in source and binary forms, with or without modification,
+   are permitted provided that the following conditions are met:
+
+   Redistributions of source code must retain the above copyright notice, this list
+   of conditions and the following disclaimer.
+
+   Redistributions in binary form must reproduce the above copyright notice, this
+   list of conditions and the following disclaimer in the documentation and/or
+   other materials provided with the distribution.
+
+   Neither the name of Loki software nor the names of its contributors may be used
+   to endorse or promote products derived from this software without specific prior
+   written permission.
+
+   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+   DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+   DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+   LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+   ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+//
+// Rules:
+//
+// - Directories should be searched in the following order: ~/.q3a/baseq3,
+//   install dir (/usr/local/games/quake3/baseq3) and cd_path (/mnt/cdrom/baseq3).
+//
+// - Pak files are searched first inside the directories.
+// - Case insensitive.
+// - Unix-style slashes (/) (windows is backwards .. everyone knows that)
+//
+// Leonardo Zide (leo@lokigames.com)
+//
+
+#include <string.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#include "cmdlib.h"
+#include "filematch.h"
+#include "mathlib.h"
+#include "inout.h"
+#include "vfs.h"
+#include <unzip.h>
+#include <glib.h>
+
+typedef struct
+{
+       char*   name;
+       unzFile zipfile;
+       unz_file_pos zippos;
+       guint32 size;
+} VFS_PAKFILE;
+
+// =============================================================================
+// Global variables
+
+static GSList*  g_unzFiles;
+static GSList*  g_pakFiles;
+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;
+
+// =============================================================================
+// Static functions
+
+static void vfsAddSlash( char *str ){
+       int n = strlen( str );
+       if ( n > 0 ) {
+               if ( str[n - 1] != '\\' && str[n - 1] != '/' ) {
+                       strcat( str, "/" );
+               }
+       }
+}
+
+static void vfsFixDOSName( char *src ){
+       if ( src == NULL ) {
+               return;
+       }
+
+       while ( *src )
+       {
+               if ( *src == '\\' ) {
+                       *src = '/';
+               }
+               src++;
+       }
+}
+
+//!\todo Define globally or use heap-allocated string.
+#define NAME_MAX 255
+
+static void vfsInitPakFile( const char *filename ){
+       unz_global_info gi;
+       unzFile uf;
+       guint32 i;
+       int err;
+
+       uf = unzOpen( filename );
+       if ( uf == NULL ) {
+               return;
+       }
+
+       g_unzFiles = g_slist_append( g_unzFiles, uf );
+
+       err = unzGetGlobalInfo( uf,&gi );
+       if ( err != UNZ_OK ) {
+               return;
+       }
+       unzGoToFirstFile( uf );
+
+       for ( i = 0; i < gi.number_entry; i++ )
+       {
+               char filename_inzip[NAME_MAX];
+               char *filename_lower;
+               unz_file_info file_info;
+               VFS_PAKFILE* file;
+
+               err = unzGetCurrentFileInfo( uf, &file_info, filename_inzip, sizeof( filename_inzip ), NULL, 0, NULL, 0 );
+               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 );
+                //-1 null terminated string
+               filename_lower = g_ascii_strdown( filename_inzip, -1 );
+
+               file->name = strdup( filename_lower );
+               file->size = file_info.uncompressed_size;
+               file->zipfile = uf;
+               file->zippos = pos;
+
+               if ( ( i + 1 ) < gi.number_entry ) {
+                       err = unzGoToNextFile( uf );
+                       if ( err != UNZ_OK ) {
+                               break;
+                       }
+               }
+               g_free( filename_lower );
+       }
+}
+
+// =============================================================================
+// Global functions
+
+// reads all pak files from a dir
+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 ) {
+               return;
+       }
+
+       Sys_Printf( "VFS Init: %s\n", 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++;
+
+       if ( g_bUsePak ) {
+               dir = g_dir_open( path, 0, NULL );
+
+               if ( dir != NULL ) {
+                       while ( 1 )
+                       {
+                               const char* name = g_dir_read_name( dir );
+                               if ( name == NULL ) {
+                                       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, ".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;
+                                       }
+                               }
+
+                               sprintf( filename, "%s/%s", path, dirlist );
+                               vfsInitPakFile( filename );
+
+                               g_free( dirlist );
+                       }
+                       g_dir_close( dir );
+               }
+       }
+}
+
+// frees all memory that we allocated
+void vfsShutdown(){
+       while ( g_unzFiles )
+       {
+               unzClose( (unzFile)g_unzFiles->data );
+               g_unzFiles = g_slist_remove( g_unzFiles, g_unzFiles->data );
+       }
+
+       while ( g_pakFiles )
+       {
+               VFS_PAKFILE* file = (VFS_PAKFILE*)g_pakFiles->data;
+               free( file->name );
+               free( file );
+               g_pakFiles = g_slist_remove( g_pakFiles, file );
+       }
+}
+
+// return the number of files that match
+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 );
+       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, lower ) == 0 ) {
+                       count++;
+               }
+       }
+
+       for ( i = 0; i < g_numDirs; i++ )
+       {
+               strcpy( tmp, g_strDirs[i] );
+               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
+       if ( index == -1 ) {
+               long len;
+               FILE *f;
+
+               f = fopen( filename, "rb" );
+               if ( f == NULL ) {
+                       return -1;
+               }
+
+               fseek( f, 0, SEEK_END );
+               len = ftell( f );
+               rewind( f );
+
+               *bufferptr = safe_malloc( len + 1 );
+               if ( *bufferptr == NULL ) {
+                       fclose( f );
+                       return -1;
+               }
+
+               if ( fread( *bufferptr, 1, len, f ) != (size_t) len ) {
+                       fclose( f );
+                       return -1;
+               }
+               fclose( f );
+
+               // we need to end the buffer with a 0
+               ( (char*) ( *bufferptr ) )[len] = 0;
+
+               return len;
+       }
+
+       *bufferptr = NULL;
+       strncpy( fixed, filename, sizeof( fixed ) );
+       vfsFixDOSName( fixed );
+       lower = g_ascii_strdown( fixed, -1 );
+
+       for ( i = 0; i < g_numDirs; i++ )
+       {
+               strcpy( tmp, g_strDirs[i] );
+               strcat( tmp, filename );
+               if ( access( tmp, R_OK ) == 0 ) {
+                       if ( count == index ) {
+                               long len;
+                               FILE *f;
+
+                               f = fopen( tmp, "rb" );
+                               if ( f == NULL ) {
+                                       return -1;
+                               }
+
+                               fseek( f, 0, SEEK_END );
+                               len = ftell( f );
+                               rewind( f );
+
+                               *bufferptr = safe_malloc( len + 1 );
+                               if ( *bufferptr == NULL ) {
+                                       fclose( f );
+                                       return -1;
+                               }
+
+                               if ( fread( *bufferptr, 1, len, f ) != (size_t) len ) {
+                                       fclose( f );
+                                       return -1;
+                               }
+                               fclose( f );
+
+                               // we need to end the buffer with a 0
+                               ( (char*) ( *bufferptr ) )[len] = 0;
+
+                               return len;
+                       }
+
+                       count++;
+               }
+       }
+
+       // 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, lower ) != 0 ) {
+                       continue;
+               }
+
+               if ( count == index ) {
+
+               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;
+                       }
+               }
+
+               count++;
+       }
+
+       g_free( lower );
+       return -1;
+}