|  |  | @@ -122,6 +122,19 @@ static int64 findCentralDirectoryFileHeader (InputStream& input, int& numEntries | 
		
	
		
			
			|  |  |  | return 0; | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | static bool hasSymbolicPart (const File& root, const File& f) | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | jassert (root == f || f.isAChildOf (root)); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | for (auto p = f; p != root; p = p.getParentDirectory()) | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | if (p.isSymbolicLink()) | 
		
	
		
			
			|  |  |  | return true; | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | return false; | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | //============================================================================== | 
		
	
		
			
			|  |  |  | struct ZipFile::ZipInputStream  : public InputStream | 
		
	
		
			
			|  |  |  | { | 
		
	
	
		
			
				|  |  | @@ -400,6 +413,14 @@ Result ZipFile::uncompressTo (const File& targetDirectory, | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | Result ZipFile::uncompressEntry (int index, const File& targetDirectory, bool shouldOverwriteFiles) | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | return uncompressEntry (index, | 
		
	
		
			
			|  |  |  | targetDirectory, | 
		
	
		
			
			|  |  |  | shouldOverwriteFiles ? OverwriteFiles::yes : OverwriteFiles::no, | 
		
	
		
			
			|  |  |  | FollowSymlinks::no); | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | Result ZipFile::uncompressEntry (int index, const File& targetDirectory, OverwriteFiles overwriteFiles, FollowSymlinks followSymlinks) | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | auto* zei = entries.getUnchecked (index); | 
		
	
		
			
			|  |  |  |  | 
		
	
	
		
			
				|  |  | @@ -414,6 +435,9 @@ Result ZipFile::uncompressEntry (int index, const File& targetDirectory, bool sh | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | auto targetFile = targetDirectory.getChildFile (entryPath); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | if (! targetFile.isAChildOf (targetDirectory)) | 
		
	
		
			
			|  |  |  | return Result::fail ("Entry " + entryPath + " is outside the target directory"); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | if (entryPath.endsWithChar ('/') || entryPath.endsWithChar ('\\')) | 
		
	
		
			
			|  |  |  | return targetFile.createDirectory(); // (entry is a directory, not a file) | 
		
	
		
			
			|  |  |  |  | 
		
	
	
		
			
				|  |  | @@ -424,13 +448,16 @@ Result ZipFile::uncompressEntry (int index, const File& targetDirectory, bool sh | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | if (targetFile.exists()) | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | if (! shouldOverwriteFiles) | 
		
	
		
			
			|  |  |  | if (overwriteFiles == OverwriteFiles::no) | 
		
	
		
			
			|  |  |  | return Result::ok(); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | if (! targetFile.deleteFile()) | 
		
	
		
			
			|  |  |  | return Result::fail ("Failed to write to target file: " + targetFile.getFullPathName()); | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | if (followSymlinks == FollowSymlinks::no && hasSymbolicPart (targetDirectory, targetFile.getParentDirectory())) | 
		
	
		
			
			|  |  |  | return Result::fail ("Parent directory leads through symlink for target file: " + targetFile.getFullPathName()); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | if (! targetFile.getParentDirectory().createDirectory()) | 
		
	
		
			
			|  |  |  | return Result::fail ("Failed to create target folder: " + targetFile.getParentDirectory().getFullPathName()); | 
		
	
		
			
			|  |  |  |  | 
		
	
	
		
			
				|  |  | @@ -649,12 +676,9 @@ struct ZIPTests   : public UnitTest | 
		
	
		
			
			|  |  |  | : UnitTest ("ZIP", UnitTestCategories::compression) | 
		
	
		
			
			|  |  |  | {} | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | void runTest() override | 
		
	
		
			
			|  |  |  | static MemoryBlock createZipMemoryBlock (const StringArray& entryNames) | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | beginTest ("ZIP"); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | ZipFile::Builder builder; | 
		
	
		
			
			|  |  |  | StringArray entryNames { "first", "second", "third" }; | 
		
	
		
			
			|  |  |  | HashMap<String, MemoryBlock> blocks; | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | for (auto& entryName : entryNames) | 
		
	
	
		
			
				|  |  | @@ -669,8 +693,61 @@ struct ZIPTests   : public UnitTest | 
		
	
		
			
			|  |  |  | MemoryBlock data; | 
		
	
		
			
			|  |  |  | MemoryOutputStream mo (data, false); | 
		
	
		
			
			|  |  |  | builder.writeToStream (mo, nullptr); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | return data; | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | void runZipSlipTest() | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | const std::map<String, bool> testCases = { { "a",                    true  }, | 
		
	
		
			
			|  |  |  | #if JUCE_WINDOWS | 
		
	
		
			
			|  |  |  | { "C:/b",                 false }, | 
		
	
		
			
			|  |  |  | #else | 
		
	
		
			
			|  |  |  | { "/b",                   false }, | 
		
	
		
			
			|  |  |  | #endif | 
		
	
		
			
			|  |  |  | { "c/d",                  true  }, | 
		
	
		
			
			|  |  |  | { "../e/f",               false }, | 
		
	
		
			
			|  |  |  | { "../../g/h",            false }, | 
		
	
		
			
			|  |  |  | { "i/../j",               true  }, | 
		
	
		
			
			|  |  |  | { "k/l/../",              true  }, | 
		
	
		
			
			|  |  |  | { "m/n/../../",           false }, | 
		
	
		
			
			|  |  |  | { "o/p/../../../",        false } }; | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | StringArray entryNames; | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | for (const auto& testCase : testCases) | 
		
	
		
			
			|  |  |  | entryNames.add (testCase.first); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | TemporaryFile tmpDir; | 
		
	
		
			
			|  |  |  | tmpDir.getFile().createDirectory(); | 
		
	
		
			
			|  |  |  | auto data = createZipMemoryBlock (entryNames); | 
		
	
		
			
			|  |  |  | MemoryInputStream mi (data, false); | 
		
	
		
			
			|  |  |  | ZipFile zip (mi); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | for (int i = 0; i < zip.getNumEntries(); ++i) | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | const auto result = zip.uncompressEntry (i, tmpDir.getFile()); | 
		
	
		
			
			|  |  |  | const auto caseIt = testCases.find (zip.getEntry (i)->filename); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | if (caseIt != testCases.end()) | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | expect (result.wasOk() == caseIt->second, | 
		
	
		
			
			|  |  |  | zip.getEntry (i)->filename + " was unexpectedly " + (result.wasOk() ? "OK" : "not OK")); | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | else | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | expect (false); | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | void runTest() override | 
		
	
		
			
			|  |  |  | { | 
		
	
		
			
			|  |  |  | beginTest ("ZIP"); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | StringArray entryNames { "first", "second", "third" }; | 
		
	
		
			
			|  |  |  | auto data = createZipMemoryBlock (entryNames); | 
		
	
		
			
			|  |  |  | MemoryInputStream mi (data, false); | 
		
	
		
			
			|  |  |  | ZipFile zip (mi); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | expectEquals (zip.getNumEntries(), entryNames.size()); | 
		
	
	
		
			
				|  |  | @@ -681,6 +758,9 @@ struct ZIPTests   : public UnitTest | 
		
	
		
			
			|  |  |  | std::unique_ptr<InputStream> input (zip.createStreamForEntry (*entry)); | 
		
	
		
			
			|  |  |  | expectEquals (input->readEntireStreamAsString(), entryName); | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | beginTest ("ZipSlip"); | 
		
	
		
			
			|  |  |  | runZipSlipTest(); | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | }; | 
		
	
		
			
			|  |  |  |  | 
		
	
	
		
			
				|  |  | 
 |