From 5a132129e9233012f1642d27fe77a412ac3a465a Mon Sep 17 00:00:00 2001 From: Mihail Feraru Date: Tue, 22 Mar 2022 13:15:54 +0200 Subject: [PATCH] Incremental block by block compression for ZIP --- src/Zip.php | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 4 deletions(-) diff --git a/src/Zip.php b/src/Zip.php index c6ba9e5..e8f3cf6 100644 --- a/src/Zip.php +++ b/src/Zip.php @@ -295,13 +295,88 @@ class Zip extends Archive throw new ArchiveIOException('Archive has been closed, files can no longer be added'); } - $data = @file_get_contents($file); - if ($data === false) { + $fp = @fopen($file, 'rb'); + if ($fp === false) { throw new ArchiveIOException('Could not open file for reading: '.$file); } - // FIXME could we stream writing compressed data? gzwrite on a fopen handle? - $this->addData($fileinfo, $data); + $offset = $this->dataOffset(); + $name = $fileinfo->getPath(); + $time = $fileinfo->getMtime(); + + // write local file header (temporary CRC and size) + $this->writebytes($this->makeLocalFileHeader( + $time, + 0, + 0, + 0, + $name, + (bool) $this->complevel + )); + + // we store no encryption header + + // prepare info, compress and write data to archive + $deflate_context = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => $this->complevel]); + $crc_context = hash_init('crc32b'); + $size = $csize = 0; + + while (!feof($fp)) { + $block = fread($fp, 512); + + if ($this->complevel) { + $is_first_block = $size === 0; + $is_last_block = feof($fp); + + if ($is_last_block) { + $c_block = deflate_add($deflate_context, $block, ZLIB_FINISH); + // get rid of the compression footer + $c_block = substr($c_block, 0, -4); + } else { + $c_block = deflate_add($deflate_context, $block, ZLIB_NO_FLUSH); + } + + // get rid of the compression header + if ($is_first_block) { + $c_block = substr($c_block, 2); + } + + $csize += strlen($c_block); + $this->writebytes($c_block); + } else { + $this->writebytes($block); + } + + $size += strlen($block); + hash_update($crc_context, $block); + } + fclose($fp); + + // update the local file header with the computed CRC and size + $crc = hexdec(hash_final($crc_context)); + $csize = $this->complevel ? $csize : $size; + $this->writebytesAt($this->makeCrcAndSize( + $crc, + $size, + $csize, + ), $offset + self::LOCAL_FILE_HEADER_CRC_OFFSET); + + // we store no data descriptor + + // add info to central file directory + $this->ctrl_dir[] = $this->makeCentralFileRecord( + $offset, + $time, + $crc, + $size, + $csize, + $name, + (bool) $this->complevel + ); + + if(is_callable($this->callback)) { + call_user_func($this->callback, $fileinfo); + } } /** @@ -709,6 +784,29 @@ class Zip extends Archive return $written; } + /** + * Write to the open filepointer or memory at the specified offset + * + * @param string $data + * @param int $offset + * @throws ArchiveIOException + * @return int number of bytes written + */ + protected function writebytesAt($data, $offset) { + if (!$this->file) { + $this->memory .= substr_replace($this->memory, $data, $offset); + $written = strlen($data); + } else { + @fseek($this->fh, $offset); + $written = @fwrite($this->fh, $data); + @fseek($this->fh, 0, SEEK_END); + } + if ($written === false) { + throw new ArchiveIOException('Failed to write to archive stream'); + } + return $written; + } + /** * Current data pointer position * @@ -825,6 +923,8 @@ class Zip extends Archive return $header; } + const LOCAL_FILE_HEADER_CRC_OFFSET = 14; + /** * Returns a local file header for the given data * @@ -865,6 +965,22 @@ class Zip extends Archive return $header; } + /** + * Returns only a part of the local file header containing the CRC, size and compressed size. + * Used to update these fields for an already written header. + * + * @param int $crc CRC32 checksum of the uncompressed data + * @param int $len length of the uncompressed data + * @param int $clen length of the compressed data + * @return string + */ + protected function makeCrcAndSize($crc, $len, $clen) { + $header = pack('V', $crc); // crc-32 + $header .= pack('V', $clen); // compressed size + $header .= pack('V', $len); // uncompressed size + return $header; + } + /** * Returns an allowed filename and an extra field header *