26inline uint16 readUnalignedLittleEndianShort (
const void* buffer)
28 auto data = readUnaligned<uint16> (buffer);
32inline uint32 readUnalignedLittleEndianInt (
const void* buffer)
34 auto data = readUnaligned<uint32> (buffer);
38struct ZipFile::ZipEntryHolder
40 ZipEntryHolder (
const char* buffer,
int fileNameLen)
42 isCompressed = readUnalignedLittleEndianShort (buffer + 10) != 0;
43 entry.fileTime = parseFileTime (readUnalignedLittleEndianShort (buffer + 12),
44 readUnalignedLittleEndianShort (buffer + 14));
45 compressedSize = (int64) readUnalignedLittleEndianInt (buffer + 20);
46 entry.uncompressedSize = (int64) readUnalignedLittleEndianInt (buffer + 24);
47 streamOffset = (int64) readUnalignedLittleEndianInt (buffer + 42);
49 entry.externalFileAttributes = readUnalignedLittleEndianInt (buffer + 38);
50 auto fileType = (entry.externalFileAttributes >> 28) & 0xf;
51 entry.isSymbolicLink = (fileType == 0xA);
56 static Time parseFileTime (uint32 time, uint32 date)
noexcept
58 auto year = (int) (1980 + (date >> 9));
59 auto month = (int) (((date >> 5) & 15) - 1);
60 auto day = (int) (date & 31);
61 auto hours = (int) time >> 11;
62 auto minutes = (int) ((time >> 5) & 63);
63 auto seconds = (int) ((time & 31) << 1);
65 return { year, month, day, hours, minutes, seconds };
69 int64 streamOffset, compressedSize;
74static int64 findCentralDirectoryFileHeader (InputStream& input,
int& numEntries)
76 BufferedInputStream in (input, 8192);
78 in.setPosition (in.getTotalLength());
79 auto pos = in.getPosition();
80 auto lowestPos = jmax ((int64) 0, pos - 1048576);
83 while (pos > lowestPos)
85 in.setPosition (pos - 22);
86 pos = in.getPosition();
87 memcpy (buffer + 22, buffer, 4);
89 if (in.read (buffer, 22) != 22)
92 for (
int i = 0; i < 22; ++i)
94 if (readUnalignedLittleEndianInt (buffer + i) == 0x06054b50)
96 in.setPosition (pos + i);
98 numEntries = readUnalignedLittleEndianShort (buffer + 10);
99 auto offset = (int64) readUnalignedLittleEndianInt (buffer + 16);
103 in.setPosition (offset);
108 if (in.readInt() != 0x02014b50)
110 in.setPosition (offset - 4);
112 if (in.readInt() == 0x02014b50)
125static bool hasSymbolicPart (
const File& root,
const File& f)
127 jassert (root == f || f.isAChildOf (root));
129 for (
auto p = f; p != root; p = p.getParentDirectory())
131 if (p.isSymbolicLink())
139struct ZipFile::ZipInputStream final :
public InputStream
141 ZipInputStream (
ZipFile& zf,
const ZipFile::ZipEntryHolder& zei)
143 zipEntryHolder (zei),
144 inputStream (zf.inputStream)
146 if (zf.inputSource !=
nullptr)
148 streamToDelete.reset (file.inputSource->createInputStream());
149 inputStream = streamToDelete.get();
154 zf.streamCounter.numOpenStreams++;
160 if (inputStream !=
nullptr
161 && inputStream->setPosition (zei.streamOffset)
162 && inputStream->read (buffer, 30) == 30
170 ~ZipInputStream()
override
173 if (inputStream !=
nullptr && inputStream == file.inputStream)
174 file.streamCounter.numOpenStreams--;
178 int64 getTotalLength()
override
180 return zipEntryHolder.compressedSize;
183 int read (
void* buffer,
int howMany)
override
188 howMany = (int) jmin ((int64) howMany, zipEntryHolder.compressedSize - pos);
190 if (inputStream ==
nullptr)
195 if (inputStream == file.inputStream)
197 const ScopedLock sl (file.lock);
198 inputStream->setPosition (pos + zipEntryHolder.streamOffset + headerSize);
199 num = inputStream->read (buffer, howMany);
203 inputStream->setPosition (pos + zipEntryHolder.streamOffset + headerSize);
204 num = inputStream->read (buffer, howMany);
211 bool isExhausted()
override
213 return headerSize <= 0 || pos >= zipEntryHolder.compressedSize;
216 int64 getPosition()
override
221 bool setPosition (int64 newPos)
override
223 pos = jlimit ((int64) 0, zipEntryHolder.compressedSize, newPos);
229 ZipEntryHolder zipEntryHolder;
232 InputStream* inputStream;
233 std::unique_ptr<InputStream> streamToDelete;
235 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ZipInputStream)
241 : inputStream (stream)
243 if (deleteStreamWhenDestroyed)
244 streamToDelete.reset (inputStream);
270ZipFile::OpenStreamCounter::~OpenStreamCounter()
277 jassert (numOpenStreams == 0);
284 return entries.
size();
289 if (
auto* zei = entries[index])
290 return &(zei->entry);
297 for (
int i = 0; i < entries.size(); ++i)
299 auto& entryFilename = entries.getUnchecked (i)->entry.filename;
301 if (ignoreCase ? entryFilename.equalsIgnoreCase (fileName)
302 : entryFilename == fileName)
311 return getEntry (getIndexOfFileName (fileName, ignoreCase));
318 if (
auto* zei = entries[index])
320 stream =
new ZipInputStream (*
this, *zei);
322 if (zei->isCompressed)
325 GZIPDecompressorInputStream::deflateFormat,
326 zei->entry.uncompressedSize);
338 for (
int i = 0; i < entries.
size(); ++i)
347 std::sort (entries.
begin(), entries.
end(),
348 [] (
const ZipEntryHolder* e1,
const ZipEntryHolder* e2) { return e1->entry.filename < e2->entry.filename; });
354 std::unique_ptr<InputStream> toDelete;
357 if (inputSource !=
nullptr)
359 in = inputSource->createInputStream();
366 auto centralDirectoryPos = findCentralDirectoryFileHeader (*in, numEntries);
368 if (centralDirectoryPos >= 0 && centralDirectoryPos < in->getTotalLength())
370 auto size = (size_t) (in->
getTotalLength() - centralDirectoryPos);
373 MemoryBlock headerData;
379 for (
int i = 0; i < numEntries; ++i)
384 auto* buffer =
static_cast<const char*
> (headerData.getData()) + pos;
385 auto fileNameLen = readUnalignedLittleEndianShort (buffer + 28u);
387 if (pos + 46 + fileNameLen > size)
390 entries.
add (
new ZipEntryHolder (buffer, fileNameLen));
392 pos += 46u + fileNameLen
393 + readUnalignedLittleEndianShort (buffer + 30u)
394 + readUnalignedLittleEndianShort (buffer + 32u);
402 const bool shouldOverwriteFiles)
404 for (
int i = 0; i < entries.
size(); ++i)
406 auto result =
uncompressEntry (i, targetDirectory, shouldOverwriteFiles);
419 shouldOverwriteFiles ? OverwriteFiles::yes : OverwriteFiles::no,
428 auto entryPath = zei->entry.filename;
430 auto entryPath = zei->entry.filename.replaceCharacter (
'\\',
'/');
433 if (entryPath.isEmpty())
436 auto targetFile = targetDirectory.
getChildFile (entryPath);
438 if (! targetFile.isAChildOf (targetDirectory))
439 return Result::fail (
"Entry " + entryPath +
" is outside the target directory");
441 if (entryPath.endsWithChar (
'/') || entryPath.endsWithChar (
'\\'))
442 return targetFile.createDirectory();
447 return Result::fail (
"Failed to open the zip file for reading");
449 if (targetFile.exists())
451 if (overwriteFiles == OverwriteFiles::no)
454 if (! targetFile.deleteFile())
455 return Result::fail (
"Failed to write to target file: " + targetFile.getFullPathName());
458 if (followSymlinks == FollowSymlinks::no && hasSymbolicPart (targetDirectory, targetFile.
getParentDirectory()))
459 return Result::fail (
"Parent directory leads through symlink for target file: " + targetFile.getFullPathName());
461 if (! targetFile.getParentDirectory().createDirectory())
462 return Result::fail (
"Failed to create target folder: " + targetFile.getParentDirectory().getFullPathName());
464 if (zei->entry.isSymbolicLink)
466 String originalFilePath (in->readEntireStreamAsString()
470 return Result::fail (
"Failed to create symbolic link: " + originalFilePath);
477 return Result::fail (
"Failed to write to target file: " + targetFile.getFullPathName());
482 targetFile.setCreationTime (zei->entry.fileTime);
483 targetFile.setLastModificationTime (zei->entry.fileTime);
484 targetFile.setLastAccessTime (zei->entry.fileTime);
491struct ZipFile::Builder::Item
494 : file (f), stream (s), storedPathname (storedPath), fileTime (time), compressionLevel (compression)
496 symbolicLink = (file.exists() && file.isSymbolicLink());
499 bool writeData (OutputStream& target,
const int64 overallStartPosition)
501 MemoryOutputStream compressedData ((
size_t) file.getSize());
507 uncompressedSize = relativePath.length();
509 checksum = zlibNamespace::crc32 (0, (uint8_t*) relativePath.toRawUTF8(), (
unsigned int) uncompressedSize);
510 compressedData << relativePath;
512 else if (compressionLevel > 0)
514 GZIPCompressorOutputStream compressor (compressedData, compressionLevel,
515 GZIPCompressorOutputStream::windowBitsRaw);
516 if (! writeSource (compressor))
521 if (! writeSource (compressedData))
525 compressedSize = (int64) compressedData.getDataSize();
526 headerStart = target.getPosition() - overallStartPosition;
528 target.writeInt (0x04034b50);
529 writeFlagsAndSizes (target);
530 target << storedPathname
536 bool writeDirectoryEntry (OutputStream& target)
538 target.writeInt (0x02014b50);
539 target.writeShort (symbolicLink ? 0x0314 : 0x0014);
540 writeFlagsAndSizes (target);
541 target.writeShort (0);
542 target.writeShort (0);
543 target.writeShort (0);
544 target.writeInt ((
int) (symbolicLink ? 0xA1ED0000 : 0));
545 target.writeInt ((
int) (uint32) headerStart);
546 target << storedPathname;
553 std::unique_ptr<InputStream> stream;
554 String storedPathname;
556 int64 compressedSize = 0, uncompressedSize = 0, headerStart = 0;
557 int compressionLevel = 0;
558 unsigned long checksum = 0;
559 bool symbolicLink =
false;
561 static void writeTimeAndDate (OutputStream& target, Time t)
563 target.writeShort ((
short) (t.getSeconds() + (t.getMinutes() << 5) + (t.getHours() << 11)));
564 target.writeShort ((
short) (t.getDayOfMonth() + ((t.getMonth() + 1) << 5) + ((t.getYear() - 1980) << 9)));
567 bool writeSource (OutputStream& target)
569 if (stream ==
nullptr)
571 stream = file.createInputStream();
573 if (stream ==
nullptr)
578 uncompressedSize = 0;
579 const int bufferSize = 4096;
580 HeapBlock<unsigned char> buffer (bufferSize);
582 while (! stream->isExhausted())
584 auto bytesRead = stream->read (buffer, bufferSize);
589 checksum = zlibNamespace::crc32 (checksum, buffer, (
unsigned int) bytesRead);
590 target.write (buffer, (
size_t) bytesRead);
591 uncompressedSize += bytesRead;
598 void writeFlagsAndSizes (OutputStream& target)
const
600 target.writeShort (10);
601 target.writeShort ((
short) (1 << 11));
602 target.writeShort ((! symbolicLink && compressionLevel > 0) ? (
short) 8 : (
short) 0);
603 writeTimeAndDate (target, fileTime);
604 target.writeInt ((
int) checksum);
605 target.writeInt ((
int) (uint32) compressedSize);
606 target.writeInt ((
int) (uint32) uncompressedSize);
607 target.writeShort (
static_cast<short> (storedPathname.toUTF8().sizeInBytes() - 1));
608 target.writeShort (0);
611 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Item)
620 items.add (
new Item (file,
nullptr, compression,
627 jassert (stream !=
nullptr);
629 items.add (
new Item ({}, stream, compression, path, time));
636 for (
int i = 0; i < items.size(); ++i)
638 if (progress !=
nullptr)
639 *progress = (i + 0.5) / items.size();
641 if (! items.getUnchecked (i)->writeData (target, fileStart))
647 for (
auto* item : items)
648 if (! item->writeDirectoryEntry (target))
658 target.
writeInt ((
int) (directoryEnd - directoryStart));
659 target.
writeInt ((
int) (directoryStart - fileStart));
662 if (progress !=
nullptr)
673struct ZIPTests final :
public UnitTest
676 :
UnitTest (
"ZIP", UnitTestCategories::compression)
684 for (
auto& entryName : entryNames)
694 MemoryOutputStream mo (data,
false);
700 void runZipSlipTest()
702 const std::map<String, bool> testCases = { {
"a",
true },
710 {
"../../g/h",
false },
713 {
"m/n/../../",
false },
714 {
"o/p/../../../",
false } };
716 StringArray entryNames;
718 for (
const auto& testCase : testCases)
719 entryNames.add (testCase.first);
721 TemporaryFile tmpDir;
722 tmpDir.getFile().createDirectory();
723 auto data = createZipMemoryBlock (entryNames);
724 MemoryInputStream mi (data,
false);
727 for (
int i = 0; i < zip.getNumEntries(); ++i)
729 const auto result = zip.uncompressEntry (i, tmpDir.getFile());
730 const auto caseIt = testCases.find (zip.getEntry (i)->filename);
732 if (caseIt != testCases.end())
734 expect (result.wasOk() == caseIt->second,
735 zip.getEntry (i)->filename +
" was unexpectedly " + (result.wasOk() ?
"OK" :
"not OK"));
744 void runTest()
override
748 StringArray entryNames {
"first",
"second",
"third" };
749 auto data = createZipMemoryBlock (entryNames);
750 MemoryInputStream mi (data,
false);
753 expectEquals (zip.getNumEntries(), entryNames.size());
755 for (
auto& entryName : entryNames)
757 auto* entry = zip.getEntry (entryName);
758 std::unique_ptr<InputStream> input (zip.createStreamForEntry (*entry));
759 expectEquals (input->readEntireStreamAsString(), entryName);
762 beginTest (
"ZipSlip");
767static ZIPTests zipTests;
static constexpr uint32 littleEndianInt(const void *bytes) noexcept
static constexpr uint16 littleEndianShort(const void *bytes) noexcept
bool failedToOpen() const noexcept
Time getLastModificationTime() const
String getFileName() const
File getChildFile(StringRef relativeOrAbsolutePath) const
bool createSymbolicLink(const File &linkFileToCreate, bool overwriteExisting) const
static juce_wchar getSeparatorChar()
File getParentDirectory() const
ValueType & getReference(KeyTypeParameter keyToLookFor)
virtual int64 getPosition()=0
virtual bool writeShort(short value)
virtual bool writeInt(int value)
int size() const noexcept
ObjectClass * getUnchecked(int index) const noexcept
void clear(bool deleteObjects=true)
ObjectClass * add(ObjectClass *newObject)
ObjectClass ** begin() noexcept
ObjectClass ** end() noexcept
static Result fail(const String &errorMessage) noexcept
static Result ok() noexcept
bool isEmpty() const noexcept
static String fromUTF8(const char *utf8buffer, int bufferSizeBytes=-1)
bool isNotEmpty() const noexcept
static Time JUCE_CALLTYPE getCurrentTime() noexcept
void addEntry(InputStream *streamToRead, int compressionLevel, const String &storedPathName, Time fileModificationTime)
bool writeToStream(OutputStream &target, double *progress) const
void addFile(const File &fileToAdd, int compressionLevel, const String &storedPathName=String())
Result uncompressTo(const File &targetDirectory, bool shouldOverwriteFiles=true)
InputStream * createStreamForEntry(int index)
const ZipEntry * getEntry(int index) const noexcept
int getNumEntries() const noexcept
ZipFile(const File &file)
Result uncompressEntry(int index, const File &targetDirectory, bool shouldOverwriteFiles=true)
int getIndexOfFileName(const String &fileName, bool ignoreCase=false) const noexcept
void sortEntriesByFilename()