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