]> git.lizzy.rs Git - irrlicht.git/blob - source/Irrlicht/CZipReader.cpp
500db9008dd35d0d711fbb3a408a500db0519cec
[irrlicht.git] / source / Irrlicht / CZipReader.cpp
1 // Copyright (C) 2002-2012 Nikolaus Gebhardt\r
2 // This file is part of the "Irrlicht Engine".\r
3 // For conditions of distribution and use, see copyright notice in irrlicht.h\r
4 \r
5 #include "CZipReader.h"\r
6 \r
7 #include "os.h"\r
8 \r
9 \r
10 #include "CFileList.h"\r
11 #include "CReadFile.h"\r
12 #include "coreutil.h"\r
13 \r
14 #include "IrrCompileConfig.h"\r
15 #include <zlib.h> // use system lib\r
16 \r
17 namespace irr\r
18 {\r
19 namespace io\r
20 {\r
21 \r
22 \r
23 // -----------------------------------------------------------------------------\r
24 // zip loader\r
25 // -----------------------------------------------------------------------------\r
26 \r
27 //! Constructor\r
28 CArchiveLoaderZIP::CArchiveLoaderZIP(io::IFileSystem* fs)\r
29 : FileSystem(fs)\r
30 {\r
31         #ifdef _DEBUG\r
32         setDebugName("CArchiveLoaderZIP");\r
33         #endif\r
34 }\r
35 \r
36 //! returns true if the file maybe is able to be loaded by this class\r
37 bool CArchiveLoaderZIP::isALoadableFileFormat(const io::path& filename) const\r
38 {\r
39         return core::hasFileExtension(filename, "zip", "pk3") ||\r
40                core::hasFileExtension(filename, "gz", "tgz");\r
41 }\r
42 \r
43 //! Check to see if the loader can create archives of this type.\r
44 bool CArchiveLoaderZIP::isALoadableFileFormat(E_FILE_ARCHIVE_TYPE fileType) const\r
45 {\r
46         return (fileType == EFAT_ZIP || fileType == EFAT_GZIP);\r
47 }\r
48 \r
49 \r
50 //! Creates an archive from the filename\r
51 /** \param file File handle to check.\r
52 \return Pointer to newly created archive, or 0 upon error. */\r
53 IFileArchive* CArchiveLoaderZIP::createArchive(const io::path& filename, bool ignoreCase, bool ignorePaths) const\r
54 {\r
55         IFileArchive *archive = 0;\r
56         io::IReadFile* file = FileSystem->createAndOpenFile(filename);\r
57 \r
58         if (file)\r
59         {\r
60                 archive = createArchive(file, ignoreCase, ignorePaths);\r
61                 file->drop();\r
62         }\r
63 \r
64         return archive;\r
65 }\r
66 \r
67 //! creates/loads an archive from the file.\r
68 //! \return Pointer to the created archive. Returns 0 if loading failed.\r
69 IFileArchive* CArchiveLoaderZIP::createArchive(io::IReadFile* file, bool ignoreCase, bool ignorePaths) const\r
70 {\r
71         IFileArchive *archive = 0;\r
72         if (file)\r
73         {\r
74                 file->seek(0);\r
75 \r
76                 u16 sig;\r
77                 file->read(&sig, 2);\r
78 \r
79 #ifdef __BIG_ENDIAN__\r
80                 sig = os::Byteswap::byteswap(sig);\r
81 #endif\r
82 \r
83                 file->seek(0);\r
84 \r
85                 bool isGZip = (sig == 0x8b1f);\r
86 \r
87                 archive = new CZipReader(FileSystem, file, ignoreCase, ignorePaths, isGZip);\r
88         }\r
89         return archive;\r
90 }\r
91 \r
92 //! Check if the file might be loaded by this class\r
93 /** Check might look into the file.\r
94 \param file File handle to check.\r
95 \return True if file seems to be loadable. */\r
96 bool CArchiveLoaderZIP::isALoadableFileFormat(io::IReadFile* file) const\r
97 {\r
98         SZIPFileHeader header;\r
99 \r
100         file->read( &header.Sig, 4 );\r
101 #ifdef __BIG_ENDIAN__\r
102         header.Sig = os::Byteswap::byteswap(header.Sig);\r
103 #endif\r
104 \r
105         return header.Sig == 0x04034b50 || // ZIP\r
106                    (header.Sig&0xffff) == 0x8b1f; // gzip\r
107 }\r
108 \r
109 // -----------------------------------------------------------------------------\r
110 // zip archive\r
111 // -----------------------------------------------------------------------------\r
112 \r
113 CZipReader::CZipReader(IFileSystem* fs, IReadFile* file, bool ignoreCase, bool ignorePaths, bool isGZip)\r
114  : CFileList((file ? file->getFileName() : io::path("")), ignoreCase, ignorePaths), FileSystem(fs), File(file), IsGZip(isGZip)\r
115 {\r
116         #ifdef _DEBUG\r
117         setDebugName("CZipReader");\r
118         #endif\r
119 \r
120         if (File)\r
121         {\r
122                 File->grab();\r
123 \r
124                 // load file entries\r
125                 if (IsGZip)\r
126                         while (scanGZipHeader()) { }\r
127                 else\r
128                         while (scanZipHeader()) { }\r
129 \r
130                 sort();\r
131         }\r
132 }\r
133 \r
134 CZipReader::~CZipReader()\r
135 {\r
136         if (File)\r
137                 File->drop();\r
138 }\r
139 \r
140 \r
141 //! get the archive type\r
142 E_FILE_ARCHIVE_TYPE CZipReader::getType() const\r
143 {\r
144         return IsGZip ? EFAT_GZIP : EFAT_ZIP;\r
145 }\r
146 \r
147 const IFileList* CZipReader::getFileList() const\r
148 {\r
149         return this;\r
150 }\r
151 \r
152 \r
153 //! scans for a local header, returns false if there is no more local file header.\r
154 //! The gzip file format seems to think that there can be multiple files in a gzip file\r
155 //! but none\r
156 bool CZipReader::scanGZipHeader()\r
157 {\r
158         SZipFileEntry entry;\r
159         entry.Offset = 0;\r
160         memset(&entry.header, 0, sizeof(SZIPFileHeader));\r
161 \r
162         // read header\r
163         SGZIPMemberHeader header;\r
164         if (File->read(&header, sizeof(SGZIPMemberHeader)) == sizeof(SGZIPMemberHeader))\r
165         {\r
166 \r
167 #ifdef __BIG_ENDIAN__\r
168                 header.sig = os::Byteswap::byteswap(header.sig);\r
169                 header.time = os::Byteswap::byteswap(header.time);\r
170 #endif\r
171 \r
172                 // check header value\r
173                 if (header.sig != 0x8b1f)\r
174                         return false;\r
175 \r
176                 // now get the file info\r
177                 if (header.flags & EGZF_EXTRA_FIELDS)\r
178                 {\r
179                         // read lenth of extra data\r
180                         u16 dataLen;\r
181 \r
182                         File->read(&dataLen, 2);\r
183 \r
184 #ifdef __BIG_ENDIAN__\r
185                         dataLen = os::Byteswap::byteswap(dataLen);\r
186 #endif\r
187 \r
188                         // skip it\r
189                         File->seek(dataLen, true);\r
190                 }\r
191 \r
192                 io::path ZipFileName = "";\r
193 \r
194                 if (header.flags & EGZF_FILE_NAME)\r
195                 {\r
196                         c8 c;\r
197                         File->read(&c, 1);\r
198                         while (c)\r
199                         {\r
200                                 ZipFileName.append(c);\r
201                                 File->read(&c, 1);\r
202                         }\r
203                 }\r
204                 else\r
205                 {\r
206                         // no file name?\r
207                         ZipFileName = Path;\r
208                         core::deletePathFromFilename(ZipFileName);\r
209 \r
210                         // rename tgz to tar or remove gz extension\r
211                         if (core::hasFileExtension(ZipFileName, "tgz"))\r
212                         {\r
213                                 ZipFileName[ ZipFileName.size() - 2] = 'a';\r
214                                 ZipFileName[ ZipFileName.size() - 1] = 'r';\r
215                         }\r
216                         else if (core::hasFileExtension(ZipFileName, "gz"))\r
217                         {\r
218                                 ZipFileName[ ZipFileName.size() - 3] = 0;\r
219                                 ZipFileName.validate();\r
220                         }\r
221                 }\r
222 \r
223                 if (header.flags & EGZF_COMMENT)\r
224                 {\r
225                         c8 c='a';\r
226                         while (c)\r
227                                 File->read(&c, 1);\r
228                 }\r
229 \r
230                 if (header.flags & EGZF_CRC16)\r
231                         File->seek(2, true);\r
232 \r
233                 // we are now at the start of the data blocks\r
234                 entry.Offset = File->getPos();\r
235 \r
236                 entry.header.FilenameLength = ZipFileName.size();\r
237 \r
238                 entry.header.CompressionMethod = header.compressionMethod;\r
239                 entry.header.DataDescriptor.CompressedSize = (File->getSize() - 8) - File->getPos();\r
240 \r
241                 // seek to file end\r
242                 File->seek(entry.header.DataDescriptor.CompressedSize, true);\r
243 \r
244                 // read CRC\r
245                 File->read(&entry.header.DataDescriptor.CRC32, 4);\r
246                 // read uncompressed size\r
247                 File->read(&entry.header.DataDescriptor.UncompressedSize, 4);\r
248 \r
249 #ifdef __BIG_ENDIAN__\r
250                 entry.header.DataDescriptor.CRC32 = os::Byteswap::byteswap(entry.header.DataDescriptor.CRC32);\r
251                 entry.header.DataDescriptor.UncompressedSize = os::Byteswap::byteswap(entry.header.DataDescriptor.UncompressedSize);\r
252 #endif\r
253 \r
254                 // now we've filled all the fields, this is just a standard deflate block\r
255                 addItem(ZipFileName, entry.Offset, entry.header.DataDescriptor.UncompressedSize, false, 0);\r
256                 FileInfo.push_back(entry);\r
257         }\r
258 \r
259         // there's only one block of data in a gzip file\r
260         return false;\r
261 }\r
262 \r
263 //! scans for a local header, returns false if there is no more local file header.\r
264 bool CZipReader::scanZipHeader(bool ignoreGPBits)\r
265 {\r
266         io::path ZipFileName = "";\r
267         SZipFileEntry entry;\r
268         entry.Offset = 0;\r
269         memset(&entry.header, 0, sizeof(SZIPFileHeader));\r
270 \r
271         File->read(&entry.header, sizeof(SZIPFileHeader));\r
272 \r
273 #ifdef __BIG_ENDIAN__\r
274                 entry.header.Sig = os::Byteswap::byteswap(entry.header.Sig);\r
275                 entry.header.VersionToExtract = os::Byteswap::byteswap(entry.header.VersionToExtract);\r
276                 entry.header.GeneralBitFlag = os::Byteswap::byteswap(entry.header.GeneralBitFlag);\r
277                 entry.header.CompressionMethod = os::Byteswap::byteswap(entry.header.CompressionMethod);\r
278                 entry.header.LastModFileTime = os::Byteswap::byteswap(entry.header.LastModFileTime);\r
279                 entry.header.LastModFileDate = os::Byteswap::byteswap(entry.header.LastModFileDate);\r
280                 entry.header.DataDescriptor.CRC32 = os::Byteswap::byteswap(entry.header.DataDescriptor.CRC32);\r
281                 entry.header.DataDescriptor.CompressedSize = os::Byteswap::byteswap(entry.header.DataDescriptor.CompressedSize);\r
282                 entry.header.DataDescriptor.UncompressedSize = os::Byteswap::byteswap(entry.header.DataDescriptor.UncompressedSize);\r
283                 entry.header.FilenameLength = os::Byteswap::byteswap(entry.header.FilenameLength);\r
284                 entry.header.ExtraFieldLength = os::Byteswap::byteswap(entry.header.ExtraFieldLength);\r
285 #endif\r
286 \r
287         if (entry.header.Sig != 0x04034b50)\r
288                 return false; // local file headers end here.\r
289 \r
290         // read filename\r
291         {\r
292                 c8 *tmp = new c8 [ entry.header.FilenameLength + 2 ];\r
293                 File->read(tmp, entry.header.FilenameLength);\r
294                 tmp[entry.header.FilenameLength] = 0;\r
295                 ZipFileName = tmp;\r
296                 delete [] tmp;\r
297         }\r
298 \r
299         if (entry.header.ExtraFieldLength)\r
300                 File->seek(entry.header.ExtraFieldLength, true);\r
301 \r
302         // if bit 3 was set, use CentralDirectory for setup\r
303         if (!ignoreGPBits && entry.header.GeneralBitFlag & ZIP_INFO_IN_DATA_DESCRIPTOR)\r
304         {\r
305                 SZIPFileCentralDirEnd dirEnd;\r
306                 FileInfo.clear();\r
307                 Files.clear();\r
308                 // First place where the end record could be stored\r
309                 File->seek(File->getSize()-22);\r
310                 const char endID[] = {0x50, 0x4b, 0x05, 0x06, 0x0};\r
311                 char tmp[5]={'\0'};\r
312                 bool found=false;\r
313                 // search for the end record ID\r
314                 while (!found && File->getPos()>0)\r
315                 {\r
316                         int seek=8;\r
317                         File->read(tmp, 4);\r
318                         switch (tmp[0])\r
319                         {\r
320                         case 0x50:\r
321                                 if (!strcmp(endID, tmp))\r
322                                 {\r
323                                         seek=4;\r
324                                         found=true;\r
325                                 }\r
326                                 break;\r
327                         case 0x4b:\r
328                                 seek=5;\r
329                                 break;\r
330                         case 0x05:\r
331                                 seek=6;\r
332                                 break;\r
333                         case 0x06:\r
334                                 seek=7;\r
335                                 break;\r
336                         }\r
337                         File->seek(-seek, true);\r
338                 }\r
339                 File->read(&dirEnd, sizeof(dirEnd));\r
340 #ifdef __BIG_ENDIAN__\r
341                 dirEnd.NumberDisk = os::Byteswap::byteswap(dirEnd.NumberDisk);\r
342                 dirEnd.NumberStart = os::Byteswap::byteswap(dirEnd.NumberStart);\r
343                 dirEnd.TotalDisk = os::Byteswap::byteswap(dirEnd.TotalDisk);\r
344                 dirEnd.TotalEntries = os::Byteswap::byteswap(dirEnd.TotalEntries);\r
345                 dirEnd.Size = os::Byteswap::byteswap(dirEnd.Size);\r
346                 dirEnd.Offset = os::Byteswap::byteswap(dirEnd.Offset);\r
347                 dirEnd.CommentLength = os::Byteswap::byteswap(dirEnd.CommentLength);\r
348 #endif\r
349                 FileInfo.reallocate(dirEnd.TotalEntries);\r
350                 File->seek(dirEnd.Offset);\r
351                 while (scanCentralDirectoryHeader()) { }\r
352                 return false;\r
353         }\r
354 \r
355         // store position in file\r
356         entry.Offset = File->getPos();\r
357         // move forward length of data\r
358         File->seek(entry.header.DataDescriptor.CompressedSize, true);\r
359 \r
360         #ifdef _DEBUG\r
361         //os::Debuginfo::print("added file from archive", ZipFileName.c_str());\r
362         #endif\r
363 \r
364         addItem(ZipFileName, entry.Offset, entry.header.DataDescriptor.UncompressedSize, ZipFileName.lastChar()=='/', FileInfo.size());\r
365         FileInfo.push_back(entry);\r
366 \r
367         return true;\r
368 }\r
369 \r
370 \r
371 //! scans for a local header, returns false if there is no more local file header.\r
372 bool CZipReader::scanCentralDirectoryHeader()\r
373 {\r
374         io::path ZipFileName = "";\r
375         SZIPFileCentralDirFileHeader entry;\r
376         File->read(&entry, sizeof(SZIPFileCentralDirFileHeader));\r
377 \r
378 #ifdef __BIG_ENDIAN__\r
379         entry.Sig = os::Byteswap::byteswap(entry.Sig);\r
380         entry.VersionMadeBy = os::Byteswap::byteswap(entry.VersionMadeBy);\r
381         entry.VersionToExtract = os::Byteswap::byteswap(entry.VersionToExtract);\r
382         entry.GeneralBitFlag = os::Byteswap::byteswap(entry.GeneralBitFlag);\r
383         entry.CompressionMethod = os::Byteswap::byteswap(entry.CompressionMethod);\r
384         entry.LastModFileTime = os::Byteswap::byteswap(entry.LastModFileTime);\r
385         entry.LastModFileDate = os::Byteswap::byteswap(entry.LastModFileDate);\r
386         entry.CRC32 = os::Byteswap::byteswap(entry.CRC32);\r
387         entry.CompressedSize = os::Byteswap::byteswap(entry.CompressedSize);\r
388         entry.UncompressedSize = os::Byteswap::byteswap(entry.UncompressedSize);\r
389         entry.FilenameLength = os::Byteswap::byteswap(entry.FilenameLength);\r
390         entry.ExtraFieldLength = os::Byteswap::byteswap(entry.ExtraFieldLength);\r
391         entry.FileCommentLength = os::Byteswap::byteswap(entry.FileCommentLength);\r
392         entry.DiskNumberStart = os::Byteswap::byteswap(entry.DiskNumberStart);\r
393         entry.InternalFileAttributes = os::Byteswap::byteswap(entry.InternalFileAttributes);\r
394         entry.ExternalFileAttributes = os::Byteswap::byteswap(entry.ExternalFileAttributes);\r
395         entry.RelativeOffsetOfLocalHeader = os::Byteswap::byteswap(entry.RelativeOffsetOfLocalHeader);\r
396 #endif\r
397 \r
398         if (entry.Sig != 0x02014b50)\r
399                 return false; // central dir headers end here.\r
400 \r
401         const long pos = File->getPos();\r
402         File->seek(entry.RelativeOffsetOfLocalHeader);\r
403         scanZipHeader(true);\r
404         File->seek(pos+entry.FilenameLength+entry.ExtraFieldLength+entry.FileCommentLength);\r
405         FileInfo.getLast().header.DataDescriptor.CompressedSize=entry.CompressedSize;\r
406         FileInfo.getLast().header.DataDescriptor.UncompressedSize=entry.UncompressedSize;\r
407         FileInfo.getLast().header.DataDescriptor.CRC32=entry.CRC32;\r
408         Files.getLast().Size=entry.UncompressedSize;\r
409         return true;\r
410 }\r
411 \r
412 \r
413 //! opens a file by file name\r
414 IReadFile* CZipReader::createAndOpenFile(const io::path& filename)\r
415 {\r
416         s32 index = findFile(filename, false);\r
417 \r
418         if (index != -1)\r
419                 return createAndOpenFile(index);\r
420 \r
421         return 0;\r
422 }\r
423 \r
424 //! opens a file by index\r
425 IReadFile* CZipReader::createAndOpenFile(u32 index)\r
426 {\r
427         // Irrlicht supports 0, 8, 12, 14, 99\r
428         //0 - The file is stored (no compression)\r
429         //1 - The file is Shrunk\r
430         //2 - The file is Reduced with compression factor 1\r
431         //3 - The file is Reduced with compression factor 2\r
432         //4 - The file is Reduced with compression factor 3\r
433         //5 - The file is Reduced with compression factor 4\r
434         //6 - The file is Imploded\r
435         //7 - Reserved for Tokenizing compression algorithm\r
436         //8 - The file is Deflated\r
437         //9 - Reserved for enhanced Deflating\r
438         //10 - PKWARE Date Compression Library Imploding\r
439         //12 - bzip2 - Compression Method from libbz2, WinZip 10\r
440         //14 - LZMA - Compression Method, WinZip 12\r
441         //96 - Jpeg compression - Compression Method, WinZip 12\r
442         //97 - WavPack - Compression Method, WinZip 11\r
443         //98 - PPMd - Compression Method, WinZip 10\r
444         //99 - AES encryption, WinZip 9\r
445 \r
446         const SZipFileEntry &e = FileInfo[Files[index].ID];\r
447         char buf[64];\r
448         s16 actualCompressionMethod=e.header.CompressionMethod;\r
449         IReadFile* decrypted=0;\r
450         u8* decryptedBuf=0;\r
451         u32 decryptedSize=e.header.DataDescriptor.CompressedSize;\r
452         switch(actualCompressionMethod)\r
453         {\r
454         case 0: // no compression\r
455                 {\r
456                         if (decrypted)\r
457                                 return decrypted;\r
458                         else\r
459                                 return createLimitReadFile(Files[index].FullName, File, e.Offset, decryptedSize);\r
460                 }\r
461         case 8:\r
462                 {\r
463                         const u32 uncompressedSize = e.header.DataDescriptor.UncompressedSize;\r
464                         c8* pBuf = new c8[ uncompressedSize ];\r
465                         if (!pBuf)\r
466                         {\r
467                                 snprintf_irr ( buf, 64, "Not enough memory for decompressing %s", Files[index].FullName.c_str() );\r
468                                 os::Printer::log( buf, ELL_ERROR);\r
469                                 if (decrypted)\r
470                                         decrypted->drop();\r
471                                 return 0;\r
472                         }\r
473 \r
474                         u8 *pcData = decryptedBuf;\r
475                         if (!pcData)\r
476                         {\r
477                                 pcData = new u8[decryptedSize];\r
478                                 if (!pcData)\r
479                                 {\r
480                                         snprintf_irr ( buf, 64, "Not enough memory for decompressing %s", Files[index].FullName.c_str() );\r
481                                         os::Printer::log( buf, ELL_ERROR);\r
482                                         delete [] pBuf;\r
483                                         return 0;\r
484                                 }\r
485 \r
486                                 //memset(pcData, 0, decryptedSize);\r
487                                 File->seek(e.Offset);\r
488                                 File->read(pcData, decryptedSize);\r
489                         }\r
490 \r
491                         // Setup the inflate stream.\r
492                         z_stream stream;\r
493                         s32 err;\r
494 \r
495                         stream.next_in = (Bytef*)pcData;\r
496                         stream.avail_in = (uInt)decryptedSize;\r
497                         stream.next_out = (Bytef*)pBuf;\r
498                         stream.avail_out = uncompressedSize;\r
499                         stream.zalloc = (alloc_func)0;\r
500                         stream.zfree = (free_func)0;\r
501 \r
502                         // Perform inflation. wbits < 0 indicates no zlib header inside the data.\r
503                         err = inflateInit2(&stream, -MAX_WBITS);\r
504                         if (err == Z_OK)\r
505                         {\r
506                                 err = inflate(&stream, Z_FINISH);\r
507                                 inflateEnd(&stream);\r
508                                 if (err == Z_STREAM_END)\r
509                                         err = Z_OK;\r
510                                 err = Z_OK;\r
511                                 inflateEnd(&stream);\r
512                         }\r
513 \r
514                         if (decrypted)\r
515                                 decrypted->drop();\r
516                         else\r
517                                 delete[] pcData;\r
518 \r
519                         if (err != Z_OK)\r
520                         {\r
521                                 snprintf_irr ( buf, 64, "Error decompressing %s", Files[index].FullName.c_str() );\r
522                                 os::Printer::log( buf, ELL_ERROR);\r
523                                 delete [] pBuf;\r
524                                 return 0;\r
525                         }\r
526                         else\r
527                                 return FileSystem->createMemoryReadFile(pBuf, uncompressedSize, Files[index].FullName, true);\r
528                 }\r
529         case 12:\r
530                 {\r
531                         os::Printer::log("bzip2 decompression not supported. File cannot be read.", ELL_ERROR);\r
532                         return 0;\r
533                 }\r
534         case 14:\r
535                 {\r
536                         os::Printer::log("lzma decompression not supported. File cannot be read.", ELL_ERROR);\r
537                         return 0;\r
538                 }\r
539         case 99:\r
540                 // If we come here with an encrypted file, decryption support is missing\r
541                 os::Printer::log("Decryption support not enabled. File cannot be read.", ELL_ERROR);\r
542                 return 0;\r
543         default:\r
544                 snprintf_irr ( buf, 64, "file has unsupported compression method. %s", Files[index].FullName.c_str() );\r
545                 os::Printer::log( buf, ELL_ERROR);\r
546                 return 0;\r
547         };\r
548 \r
549 }\r
550 \r
551 } // end namespace io\r
552 } // end namespace irr\r