From 90fdf83ca7648418f899e28490f65de53fcd31d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= <danxuliu@gmail.com>
Date: Tue, 6 Feb 2018 14:03:50 +0100
Subject: [PATCH] Use zip32 only if there are less than 65536 files
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A zip32 file can contain, at most, 65535 files (and folders), so take
that constraint into account.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
---
 lib/private/Streamer.php     | 28 +++++++++++++++++++++++----
 lib/private/legacy/files.php | 37 ++++++++++++++++++++++++++++++++----
 2 files changed, 57 insertions(+), 8 deletions(-)

diff --git a/lib/private/Streamer.php b/lib/private/Streamer.php
index 3b033e265e7..51c2c923c23 100644
--- a/lib/private/Streamer.php
+++ b/lib/private/Streamer.php
@@ -40,14 +40,34 @@ class Streamer {
 	 *
 	 * @param IRequest $request
 	 * @param int $size The size of the files in bytes
+	 * @param int $numberOfFiles The number of files (and directories) that will
+	 *        be included in the streamed file
 	 */
-	public function __construct(IRequest $request, int $size){
+	public function __construct(IRequest $request, int $size, int $numberOfFiles){
 
 		/**
-		 * If the size if below 4GB always use zip32
-		 * Use 4*1000*1000*1000 so we have a buffer for all the extra zip data
+		 * zip32 constraints for a basic (without compression, volumes nor
+		 * encryption) zip file according to the Zip specification:
+		 * - No file size is larger than 4 bytes (file size < 4294967296); see
+		 *   4.4.9 uncompressed size
+		 * - The size of all files plus their local headers is not larger than
+		 *   4 bytes; see 4.4.16 relative offset of local header and 4.4.24
+		 *   offset of start of central directory with respect to the starting
+		 *   disk number
+		 * - The total number of entries (files and directories) in the zip file
+		 *   is not larger than 2 bytes (number of entries < 65536); see 4.4.22
+		 *   total number of entries in the central dir
+		 * - The size of the central directory is not larger than 4 bytes; see
+		 *   4.4.23 size of the central directory
+		 *
+		 * Due to all that, zip32 is used if the size is below 4GB and there are
+		 * less than 65536 files; the margin between 4*1000^3 and 4*1024^3
+		 * should give enough room for the extra zip metadata. Technically, it
+		 * would still be possible to create an invalid zip32 file (for example,
+		 * a zip file from files smaller than 4GB with a central directory
+		 * larger than 4GiB), but it should not happen in the real world.
 		 */
-		if ($size < 4 * 1000 * 1000 * 1000) {
+		if ($size < 4 * 1000 * 1000 * 1000 && $numberOfFiles < 65536) {
 			$this->streamerInstance = new ZipStreamer(['zip64' => false]);
 		} else if ($request->isUserAgent($this->preferTarFor)) {
 			$this->streamerInstance = new TarStreamer();
diff --git a/lib/private/legacy/files.php b/lib/private/legacy/files.php
index b7c99b7fef8..9281c1f7da4 100644
--- a/lib/private/legacy/files.php
+++ b/lib/private/legacy/files.php
@@ -146,17 +146,23 @@ class OC_Files {
 
 			self::lockFiles($view, $dir, $files);
 
-			/* Calculate filesize */
+			/* Calculate filesize and number of files */
 			if ($getType === self::ZIP_FILES) {
+				$fileInfos = array();
 				$fileSize = 0;
 				foreach ($files as $file) {
-					$fileSize += \OC\Files\Filesystem::getFileInfo($dir . '/' . $file)->getSize();
+					$fileInfo = \OC\Files\Filesystem::getFileInfo($dir . '/' . $file);
+					$fileSize += $fileInfo->getSize();
+					$fileInfos[] = $fileInfo;
 				}
+				$numberOfFiles = self::getNumberOfFiles($fileInfos);
 			} elseif ($getType === self::ZIP_DIR) {
-				$fileSize = \OC\Files\Filesystem::getFileInfo($dir . '/' . $files)->getSize();
+				$fileInfo = \OC\Files\Filesystem::getFileInfo($dir . '/' . $files);
+				$fileSize = $fileInfo->getSize();
+				$numberOfFiles = self::getNumberOfFiles(array($fileInfo));
 			}
 
-			$streamer = new Streamer(\OC::$server->getRequest(), $fileSize);
+			$streamer = new Streamer(\OC::$server->getRequest(), $fileSize, $numberOfFiles);
 			OC_Util::obEnd();
 
 			$streamer->sendHeaders($name);
@@ -324,6 +330,29 @@ class OC_Files {
 		}
 	}
 
+	/**
+	 * Returns the total (recursive) number of files and folders in the given
+	 * FileInfos.
+	 *
+	 * @param \OCP\Files\FileInfo[] $fileInfos the FileInfos to count
+	 * @return int the total number of files and folders
+	 */
+	private static function getNumberOfFiles($fileInfos) {
+		$numberOfFiles = 0;
+
+		$view = new View();
+
+		while ($fileInfo = array_pop($fileInfos)) {
+			$numberOfFiles++;
+
+			if ($fileInfo->getType() === \OCP\Files\FileInfo::TYPE_FOLDER) {
+				$fileInfos = array_merge($fileInfos, $view->getDirectoryContent($fileInfo->getPath()));
+			}
+		}
+
+		return $numberOfFiles;
+	}
+
 	/**
 	 * @param View $view
 	 * @param string $dir
-- 
GitLab