2 Copyright (C) 2001-2006, William Joseph.
5 This file is part of GtkRadiant.
7 GtkRadiant is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
12 GtkRadiant is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with GtkRadiant; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22 #include "idatastream.h"
24 #include "bytestreamutils.h"
26 #include "modulesystem.h"
31 #include "stream/filestream.h"
32 #include "container/array.h"
33 #include "archivelib.h"
34 #include "zlibstream.h"
37 class DeflatedArchiveFile : public ArchiveFile
40 FileInputStream m_istream;
41 SubFileInputStream m_substream;
42 DeflatedInputStream m_zipstream;
43 FileInputStream::size_type m_size;
45 typedef FileInputStream::size_type size_type;
46 typedef FileInputStream::position_type position_type;
48 DeflatedArchiveFile( const char* name, const char* archiveName, position_type position, size_type stream_size, size_type file_size )
49 : m_name( name ), m_istream( archiveName ), m_substream( m_istream, position, stream_size ), m_zipstream( m_substream ), m_size( file_size ){
56 size_type size() const {
60 const char* getName() const {
61 return m_name.c_str();
64 InputStream& getInputStream(){
69 class DeflatedArchiveTextFile : public ArchiveTextFile
72 FileInputStream m_istream;
73 SubFileInputStream m_substream;
74 DeflatedInputStream m_zipstream;
75 BinaryToTextInputStream<DeflatedInputStream> m_textStream;
78 typedef FileInputStream::size_type size_type;
79 typedef FileInputStream::position_type position_type;
81 DeflatedArchiveTextFile( const char* name, const char* archiveName, position_type position, size_type stream_size )
82 : m_name( name ), m_istream( archiveName ), m_substream( m_istream, position, stream_size ), m_zipstream( m_substream ), m_textStream( m_zipstream ){
89 TextInputStream& getInputStream(){
97 #include "string/string.h"
98 #include "fs_filesystem.h"
100 class ZipArchive : public Archive
105 enum ECompressionMode
111 ZipRecord( unsigned int position, unsigned int compressed_size, unsigned int uncompressed_size, ECompressionMode mode, bool is_symlink )
112 : m_position( position ), m_stream_size( compressed_size ), m_file_size( uncompressed_size ), m_mode( mode ), m_is_symlink( is_symlink ){
115 unsigned int m_position;
116 unsigned int m_stream_size;
117 unsigned int m_file_size;
118 ECompressionMode m_mode;
120 // Do not resolve more than 5 recursive symbolic links to
121 // prevent circular symbolic links.
122 int m_max_symlink_depth = 5;
125 typedef GenericFileSystem<ZipRecord> ZipFileSystem;
126 ZipFileSystem m_filesystem;
128 FileInputStream m_istream;
130 bool is_file_symlink( unsigned int filemode ){
131 // see https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/stat.h
132 // redefine so it works outside of Unices
133 constexpr int RADIANT_S_IFMT = 00170000;
134 constexpr int RADIANT_S_IFLNK = 0120000;
135 // see https://trac.edgewall.org/attachment/ticket/8919/ZipDownload.patch
136 constexpr int PKZIP_EXTERNAL_ATTR_FILE_TYPE_SHIFT = 16;
137 unsigned long attr = filemode >> PKZIP_EXTERNAL_ATTR_FILE_TYPE_SHIFT;
138 return (attr & RADIANT_S_IFMT) == RADIANT_S_IFLNK;
143 istream_read_zip_magic( m_istream, magic );
144 if ( !( magic == zip_root_dirent_magic ) ) {
147 zip_version version_encoder;
148 istream_read_zip_version( m_istream, version_encoder );
149 zip_version version_extract;
150 istream_read_zip_version( m_istream, version_extract );
151 //unsigned short flags =
152 istream_read_int16_le( m_istream );
153 unsigned short compression_mode = istream_read_int16_le( m_istream );
154 if ( compression_mode != Z_DEFLATED && compression_mode != 0 ) {
158 istream_read_zip_dostime( m_istream, dostime );
159 //unsigned int crc32 =
160 istream_read_int32_le( m_istream );
161 unsigned int compressed_size = istream_read_uint32_le( m_istream );
162 unsigned int uncompressed_size = istream_read_uint32_le( m_istream );
163 unsigned int namelength = istream_read_uint16_le( m_istream );
164 unsigned short extras = istream_read_uint16_le( m_istream );
165 unsigned short comment = istream_read_uint16_le( m_istream );
166 //unsigned short diskstart =
167 istream_read_int16_le( m_istream );
168 //unsigned short filetype =
169 istream_read_int16_le( m_istream );
170 unsigned int filemode = istream_read_int32_le( m_istream );
171 unsigned int position = istream_read_int32_le( m_istream );
173 Array<char> filename( namelength + 1 );
174 m_istream.read( reinterpret_cast<FileInputStream::byte_type*>( filename.data() ), namelength );
175 filename[namelength] = '\0';
177 bool is_symlink = is_file_symlink( filemode );
179 // if ( is_symlink ) {
180 // globalOutputStream() << "Warning: zip archive " << makeQuoted( m_name.c_str() ) << " contains symlink file: " << makeQuoted( filename.data() ) << "\n";
183 m_istream.seek( extras + comment, FileInputStream::cur );
185 if ( path_is_directory( filename.data() ) ) {
186 m_filesystem[filename.data()] = 0;
190 ZipFileSystem::entry_type& file = m_filesystem[filename.data()];
191 if ( !file.is_directory() ) {
192 globalOutputStream() << "Warning: zip archive " << makeQuoted( m_name.c_str() ) << " contains duplicated file: " << makeQuoted( filename.data() ) << "\n";
196 file = new ZipRecord( position, compressed_size, uncompressed_size, ( compression_mode == Z_DEFLATED ) ? ZipRecord::eDeflated : ZipRecord::eStored, is_symlink );
204 SeekableStream::position_type pos = pkzip_find_disk_trailer( m_istream );
206 zip_disk_trailer disk_trailer;
207 m_istream.seek( pos );
208 istream_read_zip_disk_trailer( m_istream, disk_trailer );
209 if ( !( disk_trailer.z_magic == zip_disk_trailer_magic ) ) {
213 m_istream.seek( disk_trailer.z_rootseek );
214 for ( unsigned int i = 0; i < disk_trailer.z_entries; ++i )
216 if ( !read_record() ) {
226 ZipArchive( const char* name )
227 : m_name( name ), m_istream( name ){
228 if ( !m_istream.failed() ) {
229 if ( !read_pkzip() ) {
230 globalErrorStream() << "ERROR: invalid zip file " << makeQuoted( name ) << '\n';
236 for ( ZipFileSystem::iterator i = m_filesystem.begin(); i != m_filesystem.end(); ++i )
238 delete i->second.file();
243 return m_istream.failed();
250 // The zip format has a maximum filename size of 64K
251 static const int MAX_FILENAME_BUF = 65537;
253 /* 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.
257 - https://github.com/DaemonEngine/Daemon/blob/master/src/common/FileSystem.cpp
258 - https://gitlab.com/xonotic/darkplaces/-/blob/div0-stable/fs.c
260 Some words by slipher:
262 > Symlinks are a bad feature which you should not use. Therefore, the implementation is as
263 > slow as possible with a full iteration of the archive performed for each symlink.
265 > The symlink path `relative` must be relative to the symlink's location.
266 > Only supports paths consisting of "../" 0 or more times, followed by non-magical path components.
269 static void resolveSymlinkPath( const char* base, const char* relative, char* resolved ){
271 base = g_path_get_dirname( base );
273 while( g_str_has_prefix( relative, "../" ) )
275 if ( base[0] == '\0' )
277 globalErrorStream() << "Error while reading symbolic link " << makeQuoted( base ) << ": no such directory\n";
282 base = g_path_get_dirname( base );
286 snprintf( resolved, MAX_FILENAME_BUF, "%s/%s", base, relative);
289 ArchiveFile* readFile( const char* name, ZipRecord* file ){
290 switch ( file->m_mode )
292 case ZipRecord::eStored:
293 return StoredArchiveFile::create( name, m_name.c_str(), m_istream.tell(), file->m_stream_size, file->m_file_size );
294 case ZipRecord::eDeflated:
295 default: // silence warning about function not returning
296 return new DeflatedArchiveFile( name, m_name.c_str(), m_istream.tell(), file->m_stream_size, file->m_file_size );
300 void readSymlink( const char* name, ZipRecord* file, char* resolved ){
301 globalOutputStream() << "Found symbolic link: " << makeQuoted( name ) << "\n";
303 if ( file->m_max_symlink_depth == 0 ) {
304 globalErrorStream() << "Maximum symbolic link depth reached\n";
308 file->m_max_symlink_depth--;
310 ArchiveFile* symlink_file = readFile( name, file );
311 ScopedArchiveBuffer buffer( *symlink_file );
312 const char* relative = (const char*) buffer.buffer;
314 resolveSymlinkPath( name, relative, resolved );
315 globalOutputStream() << "Resolved symbolic link: " << makeQuoted( resolved ) << "\n";
318 ArchiveFile* openFile( const char* name ){
319 ZipFileSystem::iterator i = m_filesystem.find( name );
321 if ( i != m_filesystem.end() && !i->second.is_directory() ) {
322 ZipRecord* file = i->second.file();
324 m_istream.seek( file->m_position );
325 zip_file_header file_header;
326 istream_read_zip_file_header( m_istream, file_header );
328 if ( file_header.z_magic != zip_file_header_magic ) {
329 globalErrorStream() << "error reading zip file " << makeQuoted( m_name.c_str() );
333 if ( file->m_is_symlink ) {
334 char resolved[MAX_FILENAME_BUF];
336 readSymlink( name, file, resolved );
338 // slow as possible full iteration of the archive
339 return openFile( resolved );
342 return readFile( name, file );
348 ArchiveTextFile* readTextFile( const char* name, ZipRecord* file ){
349 switch ( file->m_mode )
351 case ZipRecord::eStored:
352 return StoredArchiveTextFile::create( name, m_name.c_str(), m_istream.tell(), file->m_stream_size );
353 case ZipRecord::eDeflated:
354 default: // silence warning about function not returning
355 return new DeflatedArchiveTextFile( name, m_name.c_str(), m_istream.tell(), file->m_stream_size );
359 ArchiveTextFile* openTextFile( const char* name ){
360 ZipFileSystem::iterator i = m_filesystem.find( name );
362 if ( i != m_filesystem.end() && !i->second.is_directory() ) {
363 ZipRecord* file = i->second.file();
365 m_istream.seek( file->m_position );
366 zip_file_header file_header;
367 istream_read_zip_file_header( m_istream, file_header );
369 if ( file_header.z_magic != zip_file_header_magic ) {
370 globalErrorStream() << "error reading zip file " << makeQuoted( m_name.c_str() );
374 if ( file->m_is_symlink ) {
375 char resolved[MAX_FILENAME_BUF];
377 readSymlink( name, file, resolved );
379 // slow as possible full iteration of the archive
380 return openTextFile( resolved );
383 return readTextFile( name, file );
390 bool containsFile( const char* name ){
391 ZipFileSystem::iterator i = m_filesystem.find( name );
392 return i != m_filesystem.end() && !i->second.is_directory();
395 void forEachFile( VisitorFunc visitor, const char* root ){
396 m_filesystem.traverse( visitor, root );
401 Archive* OpenArchive( const char* name ){
402 return new ZipArchive( name );
409 class TestVisitor : public Archive::IVisitor
412 void visit( const char* name ){
418 testzip( "c:/quake3/baseq3/mapmedia.pk3", "textures/radiant/notex.tga" );
421 void testzip( const char* name, const char* filename ){
422 Archive* archive = OpenArchive( name );
423 ArchiveFile* file = archive->openFile( filename );
425 unsigned char buffer[4096];
426 std::size_t count = file->getInputStream().read( (InputStream::byte_type*)buffer, 4096 );
430 archive->forEachFile( Archive::VisitorFunc( &visitor, Archive::eFilesAndDirectories, 0 ), "" );
431 archive->forEachFile( Archive::VisitorFunc( &visitor, Archive::eFilesAndDirectories, 1 ), "" );
432 archive->forEachFile( Archive::VisitorFunc( &visitor, Archive::eFiles, 1 ), "" );
433 archive->forEachFile( Archive::VisitorFunc( &visitor, Archive::eDirectories, 1 ), "" );
434 archive->forEachFile( Archive::VisitorFunc( &visitor, Archive::eFilesAndDirectories, 1 ), "textures" );
435 archive->forEachFile( Archive::VisitorFunc( &visitor, Archive::eFilesAndDirectories, 1 ), "textures/" );
436 archive->forEachFile( Archive::VisitorFunc( &visitor, Archive::eFilesAndDirectories, 2 ), "" );