The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

692 lines
21KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2020 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. The code included in this file is provided under the terms of the ISC license
  8. http://www.isc.org/downloads/software-support-policy/isc-license. Permission
  9. To use, copy, modify, and/or distribute this software for any purpose with or
  10. without fee is hereby granted provided that the above copyright notice and
  11. this permission notice appear in all copies.
  12. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  13. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  14. DISCLAIMED.
  15. ==============================================================================
  16. */
  17. namespace juce
  18. {
  19. inline uint16 readUnalignedLittleEndianShort (const void* buffer)
  20. {
  21. auto data = readUnaligned<uint16> (buffer);
  22. return ByteOrder::littleEndianShort (&data);
  23. }
  24. inline uint32 readUnalignedLittleEndianInt (const void* buffer)
  25. {
  26. auto data = readUnaligned<uint32> (buffer);
  27. return ByteOrder::littleEndianInt (&data);
  28. }
  29. struct ZipFile::ZipEntryHolder
  30. {
  31. ZipEntryHolder (const char* buffer, int fileNameLen)
  32. {
  33. isCompressed = readUnalignedLittleEndianShort (buffer + 10) != 0;
  34. entry.fileTime = parseFileTime (readUnalignedLittleEndianShort (buffer + 12),
  35. readUnalignedLittleEndianShort (buffer + 14));
  36. compressedSize = (int64) readUnalignedLittleEndianInt (buffer + 20);
  37. entry.uncompressedSize = (int64) readUnalignedLittleEndianInt (buffer + 24);
  38. streamOffset = (int64) readUnalignedLittleEndianInt (buffer + 42);
  39. entry.externalFileAttributes = readUnalignedLittleEndianInt (buffer + 38);
  40. auto fileType = (entry.externalFileAttributes >> 28) & 0xf;
  41. entry.isSymbolicLink = (fileType == 0xA);
  42. entry.filename = String::fromUTF8 (buffer + 46, fileNameLen);
  43. }
  44. static Time parseFileTime (uint32 time, uint32 date) noexcept
  45. {
  46. auto year = (int) (1980 + (date >> 9));
  47. auto month = (int) (((date >> 5) & 15) - 1);
  48. auto day = (int) (date & 31);
  49. auto hours = (int) time >> 11;
  50. auto minutes = (int) ((time >> 5) & 63);
  51. auto seconds = (int) ((time & 31) << 1);
  52. return { year, month, day, hours, minutes, seconds };
  53. }
  54. ZipEntry entry;
  55. int64 streamOffset, compressedSize;
  56. bool isCompressed;
  57. };
  58. //==============================================================================
  59. static int64 findCentralDirectoryFileHeader (InputStream& input, int& numEntries)
  60. {
  61. BufferedInputStream in (input, 8192);
  62. in.setPosition (in.getTotalLength());
  63. auto pos = in.getPosition();
  64. auto lowestPos = jmax ((int64) 0, pos - 1048576);
  65. char buffer[32] = {};
  66. while (pos > lowestPos)
  67. {
  68. in.setPosition (pos - 22);
  69. pos = in.getPosition();
  70. memcpy (buffer + 22, buffer, 4);
  71. if (in.read (buffer, 22) != 22)
  72. return 0;
  73. for (int i = 0; i < 22; ++i)
  74. {
  75. if (readUnalignedLittleEndianInt (buffer + i) == 0x06054b50)
  76. {
  77. in.setPosition (pos + i);
  78. in.read (buffer, 22);
  79. numEntries = readUnalignedLittleEndianShort (buffer + 10);
  80. auto offset = (int64) readUnalignedLittleEndianInt (buffer + 16);
  81. if (offset >= 4)
  82. {
  83. in.setPosition (offset);
  84. // This is a workaround for some zip files which seem to contain the
  85. // wrong offset for the central directory - instead of including the
  86. // header, they point to the byte immediately after it.
  87. if (in.readInt() != 0x02014b50)
  88. {
  89. in.setPosition (offset - 4);
  90. if (in.readInt() == 0x02014b50)
  91. offset -= 4;
  92. }
  93. }
  94. return offset;
  95. }
  96. }
  97. }
  98. return 0;
  99. }
  100. //==============================================================================
  101. struct ZipFile::ZipInputStream : public InputStream
  102. {
  103. ZipInputStream (ZipFile& zf, const ZipFile::ZipEntryHolder& zei)
  104. : file (zf),
  105. zipEntryHolder (zei),
  106. inputStream (zf.inputStream)
  107. {
  108. if (zf.inputSource != nullptr)
  109. {
  110. streamToDelete.reset (file.inputSource->createInputStream());
  111. inputStream = streamToDelete.get();
  112. }
  113. else
  114. {
  115. #if JUCE_DEBUG
  116. zf.streamCounter.numOpenStreams++;
  117. #endif
  118. }
  119. char buffer[30];
  120. if (inputStream != nullptr
  121. && inputStream->setPosition (zei.streamOffset)
  122. && inputStream->read (buffer, 30) == 30
  123. && ByteOrder::littleEndianInt (buffer) == 0x04034b50)
  124. {
  125. headerSize = 30 + ByteOrder::littleEndianShort (buffer + 26)
  126. + ByteOrder::littleEndianShort (buffer + 28);
  127. }
  128. }
  129. ~ZipInputStream() override
  130. {
  131. #if JUCE_DEBUG
  132. if (inputStream != nullptr && inputStream == file.inputStream)
  133. file.streamCounter.numOpenStreams--;
  134. #endif
  135. }
  136. int64 getTotalLength() override
  137. {
  138. return zipEntryHolder.compressedSize;
  139. }
  140. int read (void* buffer, int howMany) override
  141. {
  142. if (headerSize <= 0)
  143. return 0;
  144. howMany = (int) jmin ((int64) howMany, zipEntryHolder.compressedSize - pos);
  145. if (inputStream == nullptr)
  146. return 0;
  147. int num;
  148. if (inputStream == file.inputStream)
  149. {
  150. const ScopedLock sl (file.lock);
  151. inputStream->setPosition (pos + zipEntryHolder.streamOffset + headerSize);
  152. num = inputStream->read (buffer, howMany);
  153. }
  154. else
  155. {
  156. inputStream->setPosition (pos + zipEntryHolder.streamOffset + headerSize);
  157. num = inputStream->read (buffer, howMany);
  158. }
  159. pos += num;
  160. return num;
  161. }
  162. bool isExhausted() override
  163. {
  164. return headerSize <= 0 || pos >= zipEntryHolder.compressedSize;
  165. }
  166. int64 getPosition() override
  167. {
  168. return pos;
  169. }
  170. bool setPosition (int64 newPos) override
  171. {
  172. pos = jlimit ((int64) 0, zipEntryHolder.compressedSize, newPos);
  173. return true;
  174. }
  175. private:
  176. ZipFile& file;
  177. ZipEntryHolder zipEntryHolder;
  178. int64 pos = 0;
  179. int headerSize = 0;
  180. InputStream* inputStream;
  181. std::unique_ptr<InputStream> streamToDelete;
  182. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ZipInputStream)
  183. };
  184. //==============================================================================
  185. ZipFile::ZipFile (InputStream* stream, bool deleteStreamWhenDestroyed)
  186. : inputStream (stream)
  187. {
  188. if (deleteStreamWhenDestroyed)
  189. streamToDelete.reset (inputStream);
  190. init();
  191. }
  192. ZipFile::ZipFile (InputStream& stream) : inputStream (&stream)
  193. {
  194. init();
  195. }
  196. ZipFile::ZipFile (const File& file) : inputSource (new FileInputSource (file))
  197. {
  198. init();
  199. }
  200. ZipFile::ZipFile (InputSource* source) : inputSource (source)
  201. {
  202. init();
  203. }
  204. ZipFile::~ZipFile()
  205. {
  206. entries.clear();
  207. }
  208. #if JUCE_DEBUG
  209. ZipFile::OpenStreamCounter::~OpenStreamCounter()
  210. {
  211. /* If you hit this assertion, it means you've created a stream to read one of the items in the
  212. zipfile, but you've forgotten to delete that stream object before deleting the file..
  213. Streams can't be kept open after the file is deleted because they need to share the input
  214. stream that is managed by the ZipFile object.
  215. */
  216. jassert (numOpenStreams == 0);
  217. }
  218. #endif
  219. //==============================================================================
  220. int ZipFile::getNumEntries() const noexcept
  221. {
  222. return entries.size();
  223. }
  224. const ZipFile::ZipEntry* ZipFile::getEntry (const int index) const noexcept
  225. {
  226. if (auto* zei = entries[index])
  227. return &(zei->entry);
  228. return nullptr;
  229. }
  230. int ZipFile::getIndexOfFileName (const String& fileName, bool ignoreCase) const noexcept
  231. {
  232. for (int i = 0; i < entries.size(); ++i)
  233. {
  234. auto& entryFilename = entries.getUnchecked (i)->entry.filename;
  235. if (ignoreCase ? entryFilename.equalsIgnoreCase (fileName)
  236. : entryFilename == fileName)
  237. return i;
  238. }
  239. return -1;
  240. }
  241. const ZipFile::ZipEntry* ZipFile::getEntry (const String& fileName, bool ignoreCase) const noexcept
  242. {
  243. return getEntry (getIndexOfFileName (fileName, ignoreCase));
  244. }
  245. InputStream* ZipFile::createStreamForEntry (const int index)
  246. {
  247. InputStream* stream = nullptr;
  248. if (auto* zei = entries[index])
  249. {
  250. stream = new ZipInputStream (*this, *zei);
  251. if (zei->isCompressed)
  252. {
  253. stream = new GZIPDecompressorInputStream (stream, true,
  254. GZIPDecompressorInputStream::deflateFormat,
  255. zei->entry.uncompressedSize);
  256. // (much faster to unzip in big blocks using a buffer..)
  257. stream = new BufferedInputStream (stream, 32768, true);
  258. }
  259. }
  260. return stream;
  261. }
  262. InputStream* ZipFile::createStreamForEntry (const ZipEntry& entry)
  263. {
  264. for (int i = 0; i < entries.size(); ++i)
  265. if (&entries.getUnchecked (i)->entry == &entry)
  266. return createStreamForEntry (i);
  267. return nullptr;
  268. }
  269. void ZipFile::sortEntriesByFilename()
  270. {
  271. std::sort (entries.begin(), entries.end(),
  272. [] (const ZipEntryHolder* e1, const ZipEntryHolder* e2) { return e1->entry.filename < e2->entry.filename; });
  273. }
  274. //==============================================================================
  275. void ZipFile::init()
  276. {
  277. std::unique_ptr<InputStream> toDelete;
  278. InputStream* in = inputStream;
  279. if (inputSource != nullptr)
  280. {
  281. in = inputSource->createInputStream();
  282. toDelete.reset (in);
  283. }
  284. if (in != nullptr)
  285. {
  286. int numEntries = 0;
  287. auto centralDirectoryPos = findCentralDirectoryFileHeader (*in, numEntries);
  288. if (centralDirectoryPos >= 0 && centralDirectoryPos < in->getTotalLength())
  289. {
  290. auto size = (size_t) (in->getTotalLength() - centralDirectoryPos);
  291. in->setPosition (centralDirectoryPos);
  292. MemoryBlock headerData;
  293. if (in->readIntoMemoryBlock (headerData, (ssize_t) size) == size)
  294. {
  295. size_t pos = 0;
  296. for (int i = 0; i < numEntries; ++i)
  297. {
  298. if (pos + 46 > size)
  299. break;
  300. auto* buffer = static_cast<const char*> (headerData.getData()) + pos;
  301. auto fileNameLen = readUnalignedLittleEndianShort (buffer + 28u);
  302. if (pos + 46 + fileNameLen > size)
  303. break;
  304. entries.add (new ZipEntryHolder (buffer, fileNameLen));
  305. pos += 46u + fileNameLen
  306. + readUnalignedLittleEndianShort (buffer + 30u)
  307. + readUnalignedLittleEndianShort (buffer + 32u);
  308. }
  309. }
  310. }
  311. }
  312. }
  313. Result ZipFile::uncompressTo (const File& targetDirectory,
  314. const bool shouldOverwriteFiles)
  315. {
  316. for (int i = 0; i < entries.size(); ++i)
  317. {
  318. auto result = uncompressEntry (i, targetDirectory, shouldOverwriteFiles);
  319. if (result.failed())
  320. return result;
  321. }
  322. return Result::ok();
  323. }
  324. Result ZipFile::uncompressEntry (int index, const File& targetDirectory, bool shouldOverwriteFiles)
  325. {
  326. auto* zei = entries.getUnchecked (index);
  327. #if JUCE_WINDOWS
  328. auto entryPath = zei->entry.filename;
  329. #else
  330. auto entryPath = zei->entry.filename.replaceCharacter ('\\', '/');
  331. #endif
  332. if (entryPath.isEmpty())
  333. return Result::ok();
  334. auto targetFile = targetDirectory.getChildFile (entryPath);
  335. if (entryPath.endsWithChar ('/') || entryPath.endsWithChar ('\\'))
  336. return targetFile.createDirectory(); // (entry is a directory, not a file)
  337. std::unique_ptr<InputStream> in (createStreamForEntry (index));
  338. if (in == nullptr)
  339. return Result::fail ("Failed to open the zip file for reading");
  340. if (targetFile.exists())
  341. {
  342. if (! shouldOverwriteFiles)
  343. return Result::ok();
  344. if (! targetFile.deleteFile())
  345. return Result::fail ("Failed to write to target file: " + targetFile.getFullPathName());
  346. }
  347. if (! targetFile.getParentDirectory().createDirectory())
  348. return Result::fail ("Failed to create target folder: " + targetFile.getParentDirectory().getFullPathName());
  349. if (zei->entry.isSymbolicLink)
  350. {
  351. String originalFilePath (in->readEntireStreamAsString()
  352. .replaceCharacter (L'/', File::getSeparatorChar()));
  353. if (! File::createSymbolicLink (targetFile, originalFilePath, true))
  354. return Result::fail ("Failed to create symbolic link: " + originalFilePath);
  355. }
  356. else
  357. {
  358. FileOutputStream out (targetFile);
  359. if (out.failedToOpen())
  360. return Result::fail ("Failed to write to target file: " + targetFile.getFullPathName());
  361. out << *in;
  362. }
  363. targetFile.setCreationTime (zei->entry.fileTime);
  364. targetFile.setLastModificationTime (zei->entry.fileTime);
  365. targetFile.setLastAccessTime (zei->entry.fileTime);
  366. return Result::ok();
  367. }
  368. //==============================================================================
  369. struct ZipFile::Builder::Item
  370. {
  371. Item (const File& f, InputStream* s, int compression, const String& storedPath, Time time)
  372. : file (f), stream (s), storedPathname (storedPath), fileTime (time), compressionLevel (compression)
  373. {
  374. symbolicLink = (file.exists() && file.isSymbolicLink());
  375. }
  376. bool writeData (OutputStream& target, const int64 overallStartPosition)
  377. {
  378. MemoryOutputStream compressedData ((size_t) file.getSize());
  379. if (symbolicLink)
  380. {
  381. auto relativePath = file.getNativeLinkedTarget().replaceCharacter (File::getSeparatorChar(), L'/');
  382. uncompressedSize = relativePath.length();
  383. checksum = zlibNamespace::crc32 (0, (uint8_t*) relativePath.toRawUTF8(), (unsigned int) uncompressedSize);
  384. compressedData << relativePath;
  385. }
  386. else if (compressionLevel > 0)
  387. {
  388. GZIPCompressorOutputStream compressor (compressedData, compressionLevel,
  389. GZIPCompressorOutputStream::windowBitsRaw);
  390. if (! writeSource (compressor))
  391. return false;
  392. }
  393. else
  394. {
  395. if (! writeSource (compressedData))
  396. return false;
  397. }
  398. compressedSize = (int64) compressedData.getDataSize();
  399. headerStart = target.getPosition() - overallStartPosition;
  400. target.writeInt (0x04034b50);
  401. writeFlagsAndSizes (target);
  402. target << storedPathname
  403. << compressedData;
  404. return true;
  405. }
  406. bool writeDirectoryEntry (OutputStream& target)
  407. {
  408. target.writeInt (0x02014b50);
  409. target.writeShort (symbolicLink ? 0x0314 : 0x0014);
  410. writeFlagsAndSizes (target);
  411. target.writeShort (0); // comment length
  412. target.writeShort (0); // start disk num
  413. target.writeShort (0); // internal attributes
  414. target.writeInt ((int) (symbolicLink ? 0xA1ED0000 : 0)); // external attributes
  415. target.writeInt ((int) (uint32) headerStart);
  416. target << storedPathname;
  417. return true;
  418. }
  419. private:
  420. const File file;
  421. std::unique_ptr<InputStream> stream;
  422. String storedPathname;
  423. Time fileTime;
  424. int64 compressedSize = 0, uncompressedSize = 0, headerStart = 0;
  425. int compressionLevel = 0;
  426. unsigned long checksum = 0;
  427. bool symbolicLink = false;
  428. static void writeTimeAndDate (OutputStream& target, Time t)
  429. {
  430. target.writeShort ((short) (t.getSeconds() + (t.getMinutes() << 5) + (t.getHours() << 11)));
  431. target.writeShort ((short) (t.getDayOfMonth() + ((t.getMonth() + 1) << 5) + ((t.getYear() - 1980) << 9)));
  432. }
  433. bool writeSource (OutputStream& target)
  434. {
  435. if (stream == nullptr)
  436. {
  437. stream = file.createInputStream();
  438. if (stream == nullptr)
  439. return false;
  440. }
  441. checksum = 0;
  442. uncompressedSize = 0;
  443. const int bufferSize = 4096;
  444. HeapBlock<unsigned char> buffer (bufferSize);
  445. while (! stream->isExhausted())
  446. {
  447. auto bytesRead = stream->read (buffer, bufferSize);
  448. if (bytesRead < 0)
  449. return false;
  450. checksum = zlibNamespace::crc32 (checksum, buffer, (unsigned int) bytesRead);
  451. target.write (buffer, (size_t) bytesRead);
  452. uncompressedSize += bytesRead;
  453. }
  454. stream.reset();
  455. return true;
  456. }
  457. void writeFlagsAndSizes (OutputStream& target) const
  458. {
  459. target.writeShort (10); // version needed
  460. target.writeShort ((short) (1 << 11)); // this flag indicates UTF-8 filename encoding
  461. target.writeShort ((! symbolicLink && compressionLevel > 0) ? (short) 8 : (short) 0); //symlink target path is not compressed
  462. writeTimeAndDate (target, fileTime);
  463. target.writeInt ((int) checksum);
  464. target.writeInt ((int) (uint32) compressedSize);
  465. target.writeInt ((int) (uint32) uncompressedSize);
  466. target.writeShort (static_cast<short> (storedPathname.toUTF8().sizeInBytes() - 1));
  467. target.writeShort (0); // extra field length
  468. }
  469. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Item)
  470. };
  471. //==============================================================================
  472. ZipFile::Builder::Builder() {}
  473. ZipFile::Builder::~Builder() {}
  474. void ZipFile::Builder::addFile (const File& file, int compression, const String& path)
  475. {
  476. items.add (new Item (file, nullptr, compression,
  477. path.isEmpty() ? file.getFileName() : path,
  478. file.getLastModificationTime()));
  479. }
  480. void ZipFile::Builder::addEntry (InputStream* stream, int compression, const String& path, Time time)
  481. {
  482. jassert (stream != nullptr); // must not be null!
  483. jassert (path.isNotEmpty());
  484. items.add (new Item ({}, stream, compression, path, time));
  485. }
  486. bool ZipFile::Builder::writeToStream (OutputStream& target, double* const progress) const
  487. {
  488. auto fileStart = target.getPosition();
  489. for (int i = 0; i < items.size(); ++i)
  490. {
  491. if (progress != nullptr)
  492. *progress = (i + 0.5) / items.size();
  493. if (! items.getUnchecked (i)->writeData (target, fileStart))
  494. return false;
  495. }
  496. auto directoryStart = target.getPosition();
  497. for (auto* item : items)
  498. if (! item->writeDirectoryEntry (target))
  499. return false;
  500. auto directoryEnd = target.getPosition();
  501. target.writeInt (0x06054b50);
  502. target.writeShort (0);
  503. target.writeShort (0);
  504. target.writeShort ((short) items.size());
  505. target.writeShort ((short) items.size());
  506. target.writeInt ((int) (directoryEnd - directoryStart));
  507. target.writeInt ((int) (directoryStart - fileStart));
  508. target.writeShort (0);
  509. if (progress != nullptr)
  510. *progress = 1.0;
  511. return true;
  512. }
  513. //==============================================================================
  514. //==============================================================================
  515. #if JUCE_UNIT_TESTS
  516. struct ZIPTests : public UnitTest
  517. {
  518. ZIPTests()
  519. : UnitTest ("ZIP", UnitTestCategories::compression)
  520. {}
  521. void runTest() override
  522. {
  523. beginTest ("ZIP");
  524. ZipFile::Builder builder;
  525. StringArray entryNames { "first", "second", "third" };
  526. HashMap<String, MemoryBlock> blocks;
  527. for (auto& entryName : entryNames)
  528. {
  529. auto& block = blocks.getReference (entryName);
  530. MemoryOutputStream mo (block, false);
  531. mo << entryName;
  532. mo.flush();
  533. builder.addEntry (new MemoryInputStream (block, false), 9, entryName, Time::getCurrentTime());
  534. }
  535. MemoryBlock data;
  536. MemoryOutputStream mo (data, false);
  537. builder.writeToStream (mo, nullptr);
  538. MemoryInputStream mi (data, false);
  539. ZipFile zip (mi);
  540. expectEquals (zip.getNumEntries(), entryNames.size());
  541. for (auto& entryName : entryNames)
  542. {
  543. auto* entry = zip.getEntry (entryName);
  544. std::unique_ptr<InputStream> input (zip.createStreamForEntry (*entry));
  545. expectEquals (input->readEntireStreamAsString(), entryName);
  546. }
  547. }
  548. };
  549. static ZIPTests zipTests;
  550. #endif
  551. } // namespace juce