/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. The code included in this file is provided under the terms of the ISC license http://www.isc.org/downloads/software-support-policy/isc-license. Permission To use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted provided that the above copyright notice and this permission notice appear in all copies. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { inline uint16 readUnalignedLittleEndianShort (const void* buffer) { auto data = readUnaligned (buffer); return ByteOrder::littleEndianShort (&data); } inline uint32 readUnalignedLittleEndianInt (const void* buffer) { auto data = readUnaligned (buffer); return ByteOrder::littleEndianInt (&data); } struct ZipFile::ZipEntryHolder { ZipEntryHolder (const char* buffer, int fileNameLen) { isCompressed = readUnalignedLittleEndianShort (buffer + 10) != 0; entry.fileTime = parseFileTime (readUnalignedLittleEndianShort (buffer + 12), readUnalignedLittleEndianShort (buffer + 14)); compressedSize = (int64) readUnalignedLittleEndianInt (buffer + 20); entry.uncompressedSize = (int64) readUnalignedLittleEndianInt (buffer + 24); streamOffset = (int64) readUnalignedLittleEndianInt (buffer + 42); auto externalFileAttributes = (int32) readUnalignedLittleEndianInt (buffer + 38); auto fileType = (externalFileAttributes >> 28) & 0xf; entry.isSymbolicLink = (fileType == 0xA); entry.filename = String::fromUTF8 (buffer + 46, fileNameLen); } static Time parseFileTime (uint32 time, uint32 date) noexcept { int year = 1980 + (date >> 9); int month = ((date >> 5) & 15) - 1; int day = date & 31; int hours = time >> 11; int minutes = (time >> 5) & 63; int seconds = (int) ((time & 31) << 1); return { year, month, day, hours, minutes, seconds }; } ZipEntry entry; int64 streamOffset, compressedSize; bool isCompressed; }; //============================================================================== static int64 findCentralDirectoryFileHeader (InputStream& input, int& numEntries) { BufferedInputStream in (input, 8192); in.setPosition (in.getTotalLength()); auto pos = in.getPosition(); auto lowestPos = jmax ((int64) 0, pos - 1048576); char buffer[32] = {}; while (pos > lowestPos) { in.setPosition (pos - 22); pos = in.getPosition(); memcpy (buffer + 22, buffer, 4); if (in.read (buffer, 22) != 22) return 0; for (int i = 0; i < 22; ++i) { if (readUnalignedLittleEndianInt (buffer + i) == 0x06054b50) { in.setPosition (pos + i); in.read (buffer, 22); numEntries = readUnalignedLittleEndianShort (buffer + 10); auto offset = (int64) readUnalignedLittleEndianInt (buffer + 16); if (offset >= 4) { in.setPosition (offset); // This is a workaround for some zip files which seem to contain the // wrong offset for the central directory - instead of including the // header, they point to the byte immediately after it. if (in.readInt() != 0x02014b50) { in.setPosition (offset - 4); if (in.readInt() == 0x02014b50) offset -= 4; } } return offset; } } } return 0; } //============================================================================== struct ZipFile::ZipInputStream : public InputStream { ZipInputStream (ZipFile& zf, const ZipFile::ZipEntryHolder& zei) : file (zf), zipEntryHolder (zei), inputStream (zf.inputStream) { if (zf.inputSource != nullptr) { streamToDelete.reset (file.inputSource->createInputStream()); inputStream = streamToDelete.get(); } else { #if JUCE_DEBUG zf.streamCounter.numOpenStreams++; #endif } char buffer[30]; if (inputStream != nullptr && inputStream->setPosition (zei.streamOffset) && inputStream->read (buffer, 30) == 30 && ByteOrder::littleEndianInt (buffer) == 0x04034b50) { headerSize = 30 + ByteOrder::littleEndianShort (buffer + 26) + ByteOrder::littleEndianShort (buffer + 28); } } ~ZipInputStream() override { #if JUCE_DEBUG if (inputStream != nullptr && inputStream == file.inputStream) file.streamCounter.numOpenStreams--; #endif } int64 getTotalLength() override { return zipEntryHolder.compressedSize; } int read (void* buffer, int howMany) override { if (headerSize <= 0) return 0; howMany = (int) jmin ((int64) howMany, zipEntryHolder.compressedSize - pos); if (inputStream == nullptr) return 0; int num; if (inputStream == file.inputStream) { const ScopedLock sl (file.lock); inputStream->setPosition (pos + zipEntryHolder.streamOffset + headerSize); num = inputStream->read (buffer, howMany); } else { inputStream->setPosition (pos + zipEntryHolder.streamOffset + headerSize); num = inputStream->read (buffer, howMany); } pos += num; return num; } bool isExhausted() override { return headerSize <= 0 || pos >= zipEntryHolder.compressedSize; } int64 getPosition() override { return pos; } bool setPosition (int64 newPos) override { pos = jlimit ((int64) 0, zipEntryHolder.compressedSize, newPos); return true; } private: ZipFile& file; ZipEntryHolder zipEntryHolder; int64 pos = 0; int headerSize = 0; InputStream* inputStream; std::unique_ptr streamToDelete; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ZipInputStream) }; //============================================================================== ZipFile::ZipFile (InputStream* stream, bool deleteStreamWhenDestroyed) : inputStream (stream) { if (deleteStreamWhenDestroyed) streamToDelete.reset (inputStream); init(); } ZipFile::ZipFile (InputStream& stream) : inputStream (&stream) { init(); } ZipFile::ZipFile (const File& file) : inputSource (new FileInputSource (file)) { init(); } ZipFile::ZipFile (InputSource* source) : inputSource (source) { init(); } ZipFile::~ZipFile() { entries.clear(); } #if JUCE_DEBUG ZipFile::OpenStreamCounter::~OpenStreamCounter() { /* If you hit this assertion, it means you've created a stream to read one of the items in the zipfile, but you've forgotten to delete that stream object before deleting the file.. Streams can't be kept open after the file is deleted because they need to share the input stream that is managed by the ZipFile object. */ jassert (numOpenStreams == 0); } #endif //============================================================================== int ZipFile::getNumEntries() const noexcept { return entries.size(); } const ZipFile::ZipEntry* ZipFile::getEntry (const int index) const noexcept { if (auto* zei = entries[index]) return &(zei->entry); return nullptr; } int ZipFile::getIndexOfFileName (const String& fileName, bool ignoreCase) const noexcept { for (int i = 0; i < entries.size(); ++i) { auto& entryFilename = entries.getUnchecked (i)->entry.filename; if (ignoreCase ? entryFilename.equalsIgnoreCase (fileName) : entryFilename == fileName) return i; } return -1; } const ZipFile::ZipEntry* ZipFile::getEntry (const String& fileName, bool ignoreCase) const noexcept { return getEntry (getIndexOfFileName (fileName, ignoreCase)); } InputStream* ZipFile::createStreamForEntry (const int index) { InputStream* stream = nullptr; if (auto* zei = entries[index]) { stream = new ZipInputStream (*this, *zei); if (zei->isCompressed) { stream = new GZIPDecompressorInputStream (stream, true, GZIPDecompressorInputStream::deflateFormat, zei->entry.uncompressedSize); // (much faster to unzip in big blocks using a buffer..) stream = new BufferedInputStream (stream, 32768, true); } } return stream; } InputStream* ZipFile::createStreamForEntry (const ZipEntry& entry) { for (int i = 0; i < entries.size(); ++i) if (&entries.getUnchecked (i)->entry == &entry) return createStreamForEntry (i); return nullptr; } void ZipFile::sortEntriesByFilename() { std::sort (entries.begin(), entries.end(), [] (const ZipEntryHolder* e1, const ZipEntryHolder* e2) { return e1->entry.filename < e2->entry.filename; }); } //============================================================================== void ZipFile::init() { std::unique_ptr toDelete; InputStream* in = inputStream; if (inputSource != nullptr) { in = inputSource->createInputStream(); toDelete.reset (in); } if (in != nullptr) { int numEntries = 0; auto centralDirectoryPos = findCentralDirectoryFileHeader (*in, numEntries); if (centralDirectoryPos >= 0 && centralDirectoryPos < in->getTotalLength()) { auto size = (size_t) (in->getTotalLength() - centralDirectoryPos); in->setPosition (centralDirectoryPos); MemoryBlock headerData; if (in->readIntoMemoryBlock (headerData, (ssize_t) size) == size) { size_t pos = 0; for (int i = 0; i < numEntries; ++i) { if (pos + 46 > size) break; auto* buffer = static_cast (headerData.getData()) + pos; auto fileNameLen = readUnalignedLittleEndianShort (buffer + 28); if (pos + 46 + fileNameLen > size) break; entries.add (new ZipEntryHolder (buffer, fileNameLen)); pos += 46 + fileNameLen + readUnalignedLittleEndianShort (buffer + 30) + readUnalignedLittleEndianShort (buffer + 32); } } } } } Result ZipFile::uncompressTo (const File& targetDirectory, const bool shouldOverwriteFiles) { for (int i = 0; i < entries.size(); ++i) { auto result = uncompressEntry (i, targetDirectory, shouldOverwriteFiles); if (result.failed()) return result; } return Result::ok(); } Result ZipFile::uncompressEntry (int index, const File& targetDirectory, bool shouldOverwriteFiles) { auto* zei = entries.getUnchecked (index); #if JUCE_WINDOWS auto entryPath = zei->entry.filename; #else auto entryPath = zei->entry.filename.replaceCharacter ('\\', '/'); #endif if (entryPath.isEmpty()) return Result::ok(); auto targetFile = targetDirectory.getChildFile (entryPath); if (entryPath.endsWithChar ('/') || entryPath.endsWithChar ('\\')) return targetFile.createDirectory(); // (entry is a directory, not a file) std::unique_ptr in (createStreamForEntry (index)); if (in == nullptr) return Result::fail ("Failed to open the zip file for reading"); if (targetFile.exists()) { if (! shouldOverwriteFiles) return Result::ok(); if (! targetFile.deleteFile()) return Result::fail ("Failed to write to target file: " + targetFile.getFullPathName()); } if (! targetFile.getParentDirectory().createDirectory()) return Result::fail ("Failed to create target folder: " + targetFile.getParentDirectory().getFullPathName()); if (zei->entry.isSymbolicLink) { String originalFilePath (in->readEntireStreamAsString() .replaceCharacter (L'/', File::getSeparatorChar())); if (! File::createSymbolicLink (targetFile, originalFilePath, true)) return Result::fail ("Failed to create symbolic link: " + originalFilePath); } else { FileOutputStream out (targetFile); if (out.failedToOpen()) return Result::fail ("Failed to write to target file: " + targetFile.getFullPathName()); out << *in; } targetFile.setCreationTime (zei->entry.fileTime); targetFile.setLastModificationTime (zei->entry.fileTime); targetFile.setLastAccessTime (zei->entry.fileTime); return Result::ok(); } //============================================================================== struct ZipFile::Builder::Item { Item (const File& f, InputStream* s, int compression, const String& storedPath, Time time) : file (f), stream (s), storedPathname (storedPath), fileTime (time), compressionLevel (compression) { symbolicLink = (file.exists() && file.isSymbolicLink()); } bool writeData (OutputStream& target, const int64 overallStartPosition) { MemoryOutputStream compressedData ((size_t) file.getSize()); if (symbolicLink) { auto relativePath = file.getNativeLinkedTarget().replaceCharacter (File::getSeparatorChar(), L'/'); uncompressedSize = relativePath.length(); checksum = zlibNamespace::crc32 (0, (uint8_t*) relativePath.toRawUTF8(), (unsigned int) uncompressedSize); compressedData << relativePath; } else if (compressionLevel > 0) { GZIPCompressorOutputStream compressor (compressedData, compressionLevel, GZIPCompressorOutputStream::windowBitsRaw); if (! writeSource (compressor)) return false; } else { if (! writeSource (compressedData)) return false; } compressedSize = (int64) compressedData.getDataSize(); headerStart = target.getPosition() - overallStartPosition; target.writeInt (0x04034b50); writeFlagsAndSizes (target); target << storedPathname << compressedData; return true; } bool writeDirectoryEntry (OutputStream& target) { target.writeInt (0x02014b50); target.writeShort (symbolicLink ? 0x0314 : 0x0014); writeFlagsAndSizes (target); target.writeShort (0); // comment length target.writeShort (0); // start disk num target.writeShort (0); // internal attributes target.writeInt ((int) (symbolicLink ? 0xA1ED0000 : 0)); // external attributes target.writeInt ((int) (uint32) headerStart); target << storedPathname; return true; } private: const File file; std::unique_ptr stream; String storedPathname; Time fileTime; int64 compressedSize = 0, uncompressedSize = 0, headerStart = 0; int compressionLevel = 0; unsigned long checksum = 0; bool symbolicLink = false; static void writeTimeAndDate (OutputStream& target, Time t) { target.writeShort ((short) (t.getSeconds() + (t.getMinutes() << 5) + (t.getHours() << 11))); target.writeShort ((short) (t.getDayOfMonth() + ((t.getMonth() + 1) << 5) + ((t.getYear() - 1980) << 9))); } bool writeSource (OutputStream& target) { if (stream == nullptr) { stream.reset (file.createInputStream()); if (stream == nullptr) return false; } checksum = 0; uncompressedSize = 0; const int bufferSize = 4096; HeapBlock buffer (bufferSize); while (! stream->isExhausted()) { auto bytesRead = stream->read (buffer, bufferSize); if (bytesRead < 0) return false; checksum = zlibNamespace::crc32 (checksum, buffer, (unsigned int) bytesRead); target.write (buffer, (size_t) bytesRead); uncompressedSize += bytesRead; } stream.reset(); return true; } void writeFlagsAndSizes (OutputStream& target) const { target.writeShort (10); // version needed target.writeShort ((short) (1 << 11)); // this flag indicates UTF-8 filename encoding target.writeShort ((! symbolicLink && compressionLevel > 0) ? (short) 8 : (short) 0); //symlink target path is not compressed writeTimeAndDate (target, fileTime); target.writeInt ((int) checksum); target.writeInt ((int) (uint32) compressedSize); target.writeInt ((int) (uint32) uncompressedSize); target.writeShort ((short) storedPathname.toUTF8().sizeInBytes() - 1); target.writeShort (0); // extra field length } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Item) }; //============================================================================== ZipFile::Builder::Builder() {} ZipFile::Builder::~Builder() {} void ZipFile::Builder::addFile (const File& file, int compression, const String& path) { items.add (new Item (file, nullptr, compression, path.isEmpty() ? file.getFileName() : path, file.getLastModificationTime())); } void ZipFile::Builder::addEntry (InputStream* stream, int compression, const String& path, Time time) { jassert (stream != nullptr); // must not be null! jassert (path.isNotEmpty()); items.add (new Item ({}, stream, compression, path, time)); } bool ZipFile::Builder::writeToStream (OutputStream& target, double* const progress) const { auto fileStart = target.getPosition(); for (int i = 0; i < items.size(); ++i) { if (progress != nullptr) *progress = (i + 0.5) / items.size(); if (! items.getUnchecked (i)->writeData (target, fileStart)) return false; } auto directoryStart = target.getPosition(); for (auto* item : items) if (! item->writeDirectoryEntry (target)) return false; auto directoryEnd = target.getPosition(); target.writeInt (0x06054b50); target.writeShort (0); target.writeShort (0); target.writeShort ((short) items.size()); target.writeShort ((short) items.size()); target.writeInt ((int) (directoryEnd - directoryStart)); target.writeInt ((int) (directoryStart - fileStart)); target.writeShort (0); if (progress != nullptr) *progress = 1.0; return true; } //============================================================================== #if JUCE_UNIT_TESTS struct ZIPTests : public UnitTest { ZIPTests() : UnitTest ("ZIP") {} void runTest() override { beginTest ("ZIP"); ZipFile::Builder builder; StringArray entryNames { "first", "second", "third" }; HashMap blocks; for (auto& entryName : entryNames) { auto& block = blocks.getReference (entryName); MemoryOutputStream mo (block, false); mo << entryName; mo.flush(); builder.addEntry (new MemoryInputStream (block, false), 9, entryName, Time::getCurrentTime()); } MemoryBlock data; MemoryOutputStream mo (data, false); builder.writeToStream (mo, nullptr); MemoryInputStream mi (data, false); ZipFile zip (mi); expectEquals (zip.getNumEntries(), entryNames.size()); for (auto& entryName : entryNames) { auto* entry = zip.getEntry (entryName); std::unique_ptr input (zip.createStreamForEntry (*entry)); expectEquals (input->readEntireStreamAsString(), entryName); } } }; static ZIPTests zipTests; #endif } // namespace juce