]> de.git.xonotic.org Git - xonotic/netradiant.git/blob - plugins/archivezip/archive.cpp
Merge branch 'NateEag-master-patch-12920' into 'master'
[xonotic/netradiant.git] / plugins / archivezip / archive.cpp
1 /*
2    Copyright (C) 2001-2006, William Joseph.
3    All Rights Reserved.
4
5    This file is part of GtkRadiant.
6
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.
11
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.
16
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
20  */
21
22 #include "idatastream.h"
23 #include "cmdlib.h"
24 #include "bytestreamutils.h"
25
26 #include "modulesystem.h"
27 #include "iarchive.h"
28
29 #include <algorithm>
30 #include <glib.h>
31 #include "stream/filestream.h"
32 #include "container/array.h"
33 #include "archivelib.h"
34 #include "zlibstream.h"
35 #include "imagelib.h"
36
37 class DeflatedArchiveFile : public ArchiveFile
38 {
39 CopiedString m_name;
40 FileInputStream m_istream;
41 SubFileInputStream m_substream;
42 DeflatedInputStream m_zipstream;
43 FileInputStream::size_type m_size;
44 public:
45 typedef FileInputStream::size_type size_type;
46 typedef FileInputStream::position_type position_type;
47
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 ){
50 }
51
52 void release(){
53         delete this;
54 }
55
56 size_type size() const {
57         return m_size;
58 }
59
60 const char* getName() const {
61         return m_name.c_str();
62 }
63
64 InputStream& getInputStream(){
65         return m_zipstream;
66 }
67 };
68
69 class DeflatedArchiveTextFile : public ArchiveTextFile
70 {
71 CopiedString m_name;
72 FileInputStream m_istream;
73 SubFileInputStream m_substream;
74 DeflatedInputStream m_zipstream;
75 BinaryToTextInputStream<DeflatedInputStream> m_textStream;
76
77 public:
78 typedef FileInputStream::size_type size_type;
79 typedef FileInputStream::position_type position_type;
80
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 ){
83 }
84
85 void release(){
86         delete this;
87 }
88
89 TextInputStream& getInputStream(){
90         return m_textStream;
91 }
92 };
93
94 #include "pkzip.h"
95
96 #include <map>
97 #include "string/string.h"
98 #include "fs_filesystem.h"
99
100 class ZipArchive : public Archive
101 {
102 class ZipRecord
103 {
104 public:
105 enum ECompressionMode
106 {
107         eStored,
108         eDeflated,
109 };
110
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 ){
113 }
114
115 unsigned int m_position;
116 unsigned int m_stream_size;
117 unsigned int m_file_size;
118 ECompressionMode m_mode;
119 bool m_is_symlink;
120 // Do not resolve more than 5 recursive symbolic links to
121 // prevent circular symbolic links.
122 int m_max_symlink_depth = 5;
123 };
124
125 typedef GenericFileSystem<ZipRecord> ZipFileSystem;
126 ZipFileSystem m_filesystem;
127 CopiedString m_name;
128 FileInputStream m_istream;
129
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;
139 }
140
141 bool read_record(){
142         zip_magic magic;
143         istream_read_zip_magic( m_istream, magic );
144         if ( !( magic == zip_root_dirent_magic ) ) {
145                 return false;
146         }
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 ) {
155                 return false;
156         }
157         zip_dostime dostime;
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 );
172
173         Array<char> filename( namelength + 1 );
174         m_istream.read( reinterpret_cast<FileInputStream::byte_type*>( filename.data() ), namelength );
175         filename[namelength] = '\0';
176
177         bool is_symlink = is_file_symlink( filemode );
178
179 //      if ( is_symlink ) {
180 //              globalOutputStream() << "Warning: zip archive " << makeQuoted( m_name.c_str() ) << " contains symlink file: " << makeQuoted( filename.data() ) << "\n";
181 //      }
182
183         m_istream.seek( extras + comment, FileInputStream::cur );
184
185         if ( path_is_directory( filename.data() ) ) {
186                 m_filesystem[filename.data()] = 0;
187         }
188         else
189         {
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";
193                 }
194                 else
195                 {
196                         file = new ZipRecord( position, compressed_size, uncompressed_size, ( compression_mode == Z_DEFLATED ) ? ZipRecord::eDeflated : ZipRecord::eStored, is_symlink );
197                 }
198         }
199
200         return true;
201 }
202
203 bool read_pkzip(){
204         SeekableStream::position_type pos = pkzip_find_disk_trailer( m_istream );
205         if ( pos != 0 ) {
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 ) ) {
210                         return false;
211                 }
212
213                 m_istream.seek( disk_trailer.z_rootseek );
214                 for ( unsigned int i = 0; i < disk_trailer.z_entries; ++i )
215                 {
216                         if ( !read_record() ) {
217                                 return false;
218                         }
219                 }
220                 return true;
221         }
222         return false;
223 }
224
225 public:
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';
231                 }
232         }
233 }
234
235 ~ZipArchive(){
236         for ( ZipFileSystem::iterator i = m_filesystem.begin(); i != m_filesystem.end(); ++i )
237         {
238                 delete i->second.file();
239         }
240 }
241
242 bool failed(){
243         return m_istream.failed();
244 }
245
246 void release(){
247         delete this;
248 }
249
250 // The zip format has a maximum filename size of 64K
251 static const int MAX_FILENAME_BUF = 65537;
252
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.
254
255 See:
256
257 - https://github.com/DaemonEngine/Daemon/blob/master/src/common/FileSystem.cpp
258 - https://gitlab.com/xonotic/darkplaces/-/blob/div0-stable/fs.c
259
260 Some words by slipher:
261
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.
264
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.
267 */
268
269 static void resolveSymlinkPath( const char* base, const char* relative, char* resolved ){
270
271         base = g_path_get_dirname( base );
272
273         while( g_str_has_prefix( relative, "../" ) )
274         {
275                 if ( base[0] == '\0' )
276                 {
277                         globalErrorStream() << "Error while reading symbolic link " << makeQuoted( base ) << ": no such directory\n";
278                         resolved[0] = '\0';
279                         return;
280                 }
281
282                 base = g_path_get_dirname( base );
283                 relative += 3;
284         }
285
286         snprintf( resolved, MAX_FILENAME_BUF, "%s/%s", base, relative);
287 }
288
289 ArchiveFile* readFile( const char* name, ZipRecord* file ){
290         switch ( file->m_mode )
291         {
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 );
297         }
298 }
299
300 void readSymlink( const char* name, ZipRecord* file, char* resolved ){
301                 globalOutputStream() << "Found symbolic link: " << makeQuoted( name ) << "\n";
302
303                 if ( file->m_max_symlink_depth == 0 ) {
304                 globalErrorStream() << "Maximum symbolic link depth reached\n";
305                         return;
306                 }
307
308                 file->m_max_symlink_depth--;
309
310                 ArchiveFile* symlink_file = readFile( name, file );
311                 ScopedArchiveBuffer buffer( *symlink_file );
312                 const char* relative = (const char*) buffer.buffer;
313
314                 resolveSymlinkPath( name, relative, resolved );
315                 globalOutputStream() << "Resolved symbolic link: " << makeQuoted( resolved ) << "\n";
316 }
317
318 ArchiveFile* openFile( const char* name ){
319         ZipFileSystem::iterator i = m_filesystem.find( name );
320
321         if ( i != m_filesystem.end() && !i->second.is_directory() ) {
322                 ZipRecord* file = i->second.file();
323
324                 m_istream.seek( file->m_position );
325                 zip_file_header file_header;
326                 istream_read_zip_file_header( m_istream, file_header );
327
328                 if ( file_header.z_magic != zip_file_header_magic ) {
329                         globalErrorStream() << "error reading zip file " << makeQuoted( m_name.c_str() );
330                         return 0;
331                 }
332
333                 if ( file->m_is_symlink ) {
334                         char resolved[MAX_FILENAME_BUF];
335
336                         readSymlink( name, file, resolved );
337
338                         // slow as possible full iteration of the archive
339                         return openFile( resolved );
340                 }
341
342                 return readFile( name, file );
343         }
344
345         return 0;
346 }
347
348 ArchiveTextFile* readTextFile( const char* name, ZipRecord* file ){
349         switch ( file->m_mode )
350         {
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 );
356         }
357 }
358
359 ArchiveTextFile* openTextFile( const char* name ){
360         ZipFileSystem::iterator i = m_filesystem.find( name );
361
362         if ( i != m_filesystem.end() && !i->second.is_directory() ) {
363                 ZipRecord* file = i->second.file();
364
365                 m_istream.seek( file->m_position );
366                 zip_file_header file_header;
367                 istream_read_zip_file_header( m_istream, file_header );
368
369                 if ( file_header.z_magic != zip_file_header_magic ) {
370                         globalErrorStream() << "error reading zip file " << makeQuoted( m_name.c_str() );
371                         return 0;
372                 }
373
374                 if ( file->m_is_symlink ) {
375                         char resolved[MAX_FILENAME_BUF];
376
377                         readSymlink( name, file, resolved );
378
379                         // slow as possible full iteration of the archive
380                         return openTextFile( resolved );
381                 }
382
383                 return readTextFile( name, file );
384         }
385
386         return 0;
387
388 }
389
390 bool containsFile( const char* name ){
391         ZipFileSystem::iterator i = m_filesystem.find( name );
392         return i != m_filesystem.end() && !i->second.is_directory();
393
394 }
395 void forEachFile( VisitorFunc visitor, const char* root ){
396         m_filesystem.traverse( visitor, root );
397 }
398
399 };
400
401 Archive* OpenArchive( const char* name ){
402         return new ZipArchive( name );
403 }
404
405 #if 0
406
407 class TestZip
408 {
409 class TestVisitor : public Archive::IVisitor
410 {
411 public:
412 void visit( const char* name ){
413         int bleh = 0;
414 }
415 };
416 public:
417 TestZip(){
418         testzip( "c:/quake3/baseq3/mapmedia.pk3", "textures/radiant/notex.tga" );
419 }
420
421 void testzip( const char* name, const char* filename ){
422         Archive* archive = OpenArchive( name );
423         ArchiveFile* file = archive->openFile( filename );
424         if ( file != 0 ) {
425                 unsigned char buffer[4096];
426                 std::size_t count = file->getInputStream().read( (InputStream::byte_type*)buffer, 4096 );
427                 file->release();
428         }
429         TestVisitor visitor;
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 ), "" );
437         archive->release();
438 }
439 };
440
441 TestZip g_TestZip;
442
443 #endif