diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44a9f6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/composer.phar +/vendor/ +/composer.lock diff --git a/.travis.yml b/.travis.yml index 39a9183..1bd5181 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,37 +1,31 @@ language: php -sudo: false +dist: trusty +sudo: required + +services: + - docker env: - - PECL_HTTP_VERSION=1.7.6 - - PECL_HTTP_VERSION=2.5.0 - - PECL_HTTP_VERSION=3.0.0RC1 + - PECL_HTTP_VERSION=3.1.1RC1 php: + - 7.2 + - 7.1 - 7.0 - - 5.6 - - 5.5 - - 5.4 - - 5.3 before_install: - - sh -c "mkdir -p ${TRAVIS_BUILD_DIR}/travis/module-cache/`php-config --vernum`" - -before_script: - - ./test/travis/php-modules-install.sh + - docker build -t datawraith/p7zip test/integration + - composer install --dev + - sudo apt-get install unzip libgnutls-dev + - pecl channel-update pecl.php.net + - printf "\n\n\n" | pecl install pecl_http-$PECL_HTTP_VERSION script: - - phpunit --configuration test/phpunit.xml - -cache: - directories: - - ${TRAVIS_BUILD_DIR}/travis/module-cache + - vendor/bin/phpunit --configuration test/phpunit.xml matrix: fast_finish: true - exclude: - - php: 5.5 - env: PECL_HTTP_VERSION=1.7.6 + include: - php: 5.6 - env: PECL_HTTP_VERSION=1.7.6 - - php: 7.0 - env: PECL_HTTP_VERSION=1.7.6 + env: PECL_HTTP_VERSION=2.6.0 + diff --git a/composer.json b/composer.json index d9c0e44..ecbf5ea 100644 --- a/composer.json +++ b/composer.json @@ -1,10 +1,11 @@ { - "name": "mcnetic/zipstreamer", + "name": "deepdiver/zipstreamer", "type": "library", "description": "Stream zip files without i/o overhead", "keywords": ["zip", "stream"], - "homepage": "https://github.com/McNetic/PHPZipStreamer", + "homepage": "https://github.com/DeepDiver1975/PHPZipStreamer", "license": "GPL-3.0+", + "version": "1.1.0", "authors": [ { "name": "Nicolai Ehemann", "email": "en@enlightened.de", @@ -17,19 +18,30 @@ "name": "Lukas Reschke", "email": "lukas@owncloud.com", "role": "Contributor" + },{ + "name": "Thomas Müller", + "email": "thomas.mueller@tmit.eu", + "role": "Contributor" + },{ + "name": "Roeland Jago Douma", + "email": "roeland@famdouma.nl", + "role": "Contributor" } ], "repositories": [ { "type": "vcs", - "url": "https://github.com/McNetic/PHPZipStreamer" + "url": "https://github.com/DeepDiver1975/PHPZipStreamer" } ], "require": { - "php": ">=5.3.0" + "php": ">=5.6.0" }, "autoload": { "psr-4": { "ZipStreamer\\": "src/" } + }, + "require-dev": { + "phpunit/phpunit": "^5.7" } } diff --git a/src/ZipStreamer.php b/src/ZipStreamer.php index 18d41a6..656413b 100644 --- a/src/ZipStreamer.php +++ b/src/ZipStreamer.php @@ -31,7 +31,7 @@ */ namespace ZipStreamer; -require "lib/Count64.php"; +require_once __DIR__ . "/lib/Count64.php"; class COMPR { // compression method @@ -49,6 +49,7 @@ class ZipStreamer { const VERSION = "1.0"; const ZIP_LOCAL_FILE_HEADER = 0x04034b50; // local file header signature + const ZIP_DATA_DESCRIPTOR_HEADER = 0x08074b50; //data descriptor header signature const ZIP_CENTRAL_FILE_HEADER = 0x02014b50; // central file header signature const ZIP_END_OF_CENTRAL_DIRECTORY = 0x06054b50; // end of central directory record const ZIP64_END_OF_CENTRAL_DIRECTORY = 0x06064b50; //zip64 end of central directory record @@ -76,8 +77,10 @@ class ZipStreamer { private $offset; /** @var boolean indicates zip is finalized and sent to client; no further addition possible */ private $isFinalized = false; + /** @var bool only used for unit testing */ + public $turnOffOutputBuffering = true; - /** + /** * Constructor. Initializes ZipStreamer object for immediate usage. * @param array $options Optional, ZipStreamer and zip file options as key/value pairs. * Valid options are: @@ -178,7 +181,9 @@ public function sendHeaders($archiveName = 'archive.zip', $contentType = 'applic } $this->flush(); // turn off output buffering - @ob_end_flush(); + if ($this->turnOffOutputBuffering) { + @ob_end_flush(); + } } /** @@ -427,16 +432,17 @@ private function buildLocalFileHeader($filePath, $timestamp, $gpFlags, private function addDataDescriptor($dataLength, $gzLength, $dataCRC32) { if ($this->zip64) { - $length = 20; + $length = 24; $packedGzLength = pack64le($gzLength); $packedDataLength = pack64le($dataLength); } else { - $length = 12; + $length = 16; $packedGzLength = pack32le($gzLength->getLoBytes()); $packedDataLength = pack32le($dataLength->getLoBytes()); } $this->write('' + . pack32le(self::ZIP_DATA_DESCRIPTOR_HEADER) // data descriptor header signature 4 bytes (0x08074b50) . pack32le($dataCRC32) // crc-32 4 bytes . $packedGzLength // compressed size 4/8 bytes (depending on zip64 enabled) . $packedDataLength // uncompressed size 4/8 bytes (depending on zip64 enabled) @@ -525,7 +531,7 @@ private function buildCentralDirectoryHeader($filePath, $timestamp, $gpFlags, private function buildEndOfCentralDirectoryRecord($cdRecLength) { if ($this->zip64) { $diskNumber = -1; - $cdRecCount = -1; + $cdRecCount = min(sizeof($this->cdRec), 0xffff); $cdRecLength = -1; $offset = -1; } else { diff --git a/src/lib/Count64.php b/src/lib/Count64.php index b52db06..4db8bae 100644 --- a/src/lib/Count64.php +++ b/src/lib/Count64.php @@ -300,5 +300,3 @@ public static function construct($value = 0, $limit32Bit = False) { } } } - -?> diff --git a/test/ZipComponents.php b/test/ZipComponents.php index 445c9ee..ac197d6 100644 --- a/test/ZipComponents.php +++ b/test/ZipComponents.php @@ -8,8 +8,6 @@ */ namespace ZipStreamer; -require_once "src/ZipStreamer.php"; - /** * @codeCoverageIgnore */ @@ -471,9 +469,9 @@ public function readFromString($str, $pos, $size = -1) { } if (GPFLAGS::ADD & $this->lfh->gpFlags) { if (is_null($this->lfh->z64Ext)) { - $ddLength = 12; + $ddLength = 16; } else { - $ddLength = 20; + $ddLength = 24; } $this->dd = DataDescriptor::constructFromString($str, $pos, $ddLength); $pos = $this->dd->end + 1; @@ -570,6 +568,7 @@ public function readFromString($str, $pos, $size = -1) { * @codeCoverageIgnore */ class DataDescriptor extends zipRecord { + protected static $MAGIC = 0x08074b50; // data descriptor header signature protected static $shortName = "DD"; public $dataCRC32; public $sizeCompressed; @@ -586,13 +585,23 @@ public function __toString() { } public static function constructFromString($str, $offset = 0, $size = -1) { + $ddheadPos = strpos($str, static::getMagicBytes(), $offset); + if (self::$unitTest) { + self::$unitTest->assertFalse(False === $ddheadPos, "data descriptor header missing"); + self::$unitTest->assertEquals($offset, $ddheadPos, "garbage before data descriptor header"); + } + return static::__constructFromString($str, $offset, $size); } public function readFromString($str, $pos, $size = -1) { $this->begin = $pos; + $magic = readstr($str, $pos, 4); + if (static::getMagicBytes() != $magic) { + throw new ParseException("invalid magic"); + } $this->dataCRC32 = (int) unpack32le(readstr($str, $pos, 4)); - if (20 == $size) { + if (24 == $size) { $this->sizeCompressed = unpack64le(readstr($str, $pos, 8)); $this->size = unpack64le(readstr($str, $pos, 8)); } else { diff --git a/test/ZipStreamerTest.php b/test/ZipStreamerTest.php index 64d5d99..37a874d 100644 --- a/test/ZipStreamerTest.php +++ b/test/ZipStreamerTest.php @@ -8,8 +8,8 @@ */ namespace ZipStreamer; -require "src/ZipStreamer.php"; -require "test/ZipComponents.php"; +require_once __DIR__ . "/../src/ZipStreamer.php"; +require_once __DIR__ . "/ZipComponents.php"; class File { const FILE = 1; @@ -65,12 +65,12 @@ protected static function getVersionToExtract($zip64, $isDir) { } protected function assertOutputEqualsFile($filename) { - return $this->assertEquals(file_get_contents($filename), $this->getOutput()); + $this->assertEquals(file_get_contents($filename), $this->getOutput()); } protected function assertContainsOneMatch($pattern, $input) { $results = preg_grep($pattern, $input); - return $this->assertEquals(1, sizeof($results)); + $this->assertEquals(1, sizeof($results)); } protected function assertOutputZipfileOK($files, $options) { @@ -88,8 +88,8 @@ protected function assertOutputZipfileOK($files, $options) { $eocdrec->assertValues(array( "numberDisk" => 0xffff, "numberDiskStartCD" => 0xffff, - "numberEntriesDisk" => 0xffff, - "numberEntriesCD" => 0xffff, + "numberEntriesDisk" => sizeof($files), + "numberEntriesCD" => sizeof($files), "size" => 0xffffffff, "offsetStart" => 0xffffffff, "lengthComment" => 0, @@ -311,6 +311,7 @@ public function testSendHeadersOKWithRegularBrowser(array $arguments, $zip = new ZipStreamer(array( 'outstream' => $this->outstream )); + $zip->turnOffOutputBuffering = false; $_SERVER['HTTP_USER_AGENT'] = $browser; call_user_func_array(array($zip, "sendHeaders"), $arguments); $headers = xdebug_get_headers(); diff --git a/test/integration/Dockerfile b/test/integration/Dockerfile new file mode 100644 index 0000000..03ff843 --- /dev/null +++ b/test/integration/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine:3.6 +MAINTAINER Johannes Holzfuß + +# This Dockerfile containerizes p7zip. +# +# You must run it using the correct UID/GID via the -u switch to `docker run` +# or the permissions will be wrong. +# +# Example usage +# docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd)":/data datawraith/p7zip a archive.7z file1 file2 file3 + +RUN apk --update add \ + p7zip \ + && rm -rf /var/cache/apk/* + +RUN mkdir /data +WORKDIR /data + +ENTRYPOINT ["7z"] diff --git a/test/integration/UnpackTest.php b/test/integration/UnpackTest.php new file mode 100644 index 0000000..45db8d7 --- /dev/null +++ b/test/integration/UnpackTest.php @@ -0,0 +1,49 @@ + + * + */ + +class UnpackTest extends PHPUnit_Framework_TestCase +{ + private $tmpfname; + + function setUp() + { + parent::setUp(); + + // create a zip file in tmp folder + $this->tmpfname = tempnam("/tmp", "FOO"); + $outstream = fopen($this->tmpfname, 'w'); + + $zip = new ZipStreamer\ZipStreamer((array( + 'outstream' => $outstream + ))); + $stream = fopen(__DIR__ . "/../../README.md", "r"); + $zip->addFileFromStream($stream, 'README.test'); + fclose($stream); + $zip->finalize(); + + fflush($outstream); + fclose($outstream); + } + + public function test7zip() { + $output = []; + $return_var = -1; + exec('docker run --rm -u "$(id -u):$(id -g)" -v /tmp:/data datawraith/p7zip t ' . escapeshellarg(basename($this->tmpfname)), $output, $return_var); + $fullOutput = implode("\n", $output); + $this->assertEquals($output[1], '7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21', $fullOutput); + $this->assertEquals(0, $return_var, $fullOutput); + $this->assertTrue(in_array('1 file, 943 bytes (1 KiB)', $output), $fullOutput); + } + + public function testUnzip() { + $output = []; + $return_var = -1; + exec('unzip -t ' . escapeshellarg($this->tmpfname), $output, $return_var); + $fullOutput = implode("\n", $output); + $this->assertEquals(0, $return_var, $fullOutput); + $this->assertTrue(in_array(' testing: README.test OK', $output), $fullOutput); + } +} diff --git a/test/lib/Count64Test.php b/test/lib/Count64Test.php index 0508506..8e127ce 100644 --- a/test/lib/Count64Test.php +++ b/test/lib/Count64Test.php @@ -5,6 +5,9 @@ * This file is licensed under the GNU GPL version 3 or later. * See COPYING for details. */ + +require_once __DIR__ . '/../../src/lib/Count64.php'; + class TestPack extends PHPUnit_Framework_TestCase { public function providerPack16leValues() { diff --git a/test/phpunit.xml b/test/phpunit.xml index f367297..d73bdbe 100644 --- a/test/phpunit.xml +++ b/test/phpunit.xml @@ -6,6 +6,8 @@ debug="true" > - ./ + ZipStreamerTest.php + lib + integration diff --git a/test/travis/php-modules-install.sh b/test/travis/php-modules-install.sh deleted file mode 100755 index 4543a60..0000000 --- a/test/travis/php-modules-install.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -MODULE_CACHE_DIR=${TRAVIS_BUILD_DIR}/travis/module-cache/$(php-config --vernum) -PHP_CONFIG=${TRAVIS_BUILD_DIR}/travis/phpconfig.ini -PHP_TARGET_DIR=$(php-config --extension-dir) - -if [ -d ${MODULE_CACHE_DIR} ]; then - cp ${MODULE_CACHE_DIR}/* ${PHP_TARGET_DIR} -fi - -touch ${PHP_CONFIG} -mkdir -p ${MODULE_CACHE_DIR} - -pecl_module_install() { - if [[ "-f" = $1 ]]; then - force="-f" - shift - else - force="" - fi - package=$1 - filename=$2 - - if php -m | grep $package > /dev/null; - then - echo "$package already installed and active" - elif [ ! -f ${PHP_TARGET_DIR}/${filename} ] - then - echo "$filename not found in extension dir, compiling" - pecl install $force ${package} - else - echo "Adding $filename to php config" - echo "extension = $filename" >> ${PHP_CONFIG} - fi - cp ${PHP_TARGET_DIR}/${filename} ${MODULE_CACHE_DIR} -} - -if [[ 7 -le ${PECL_HTTP_VERSION%%.*} ]]; then - yes | pecl_module_install raphf raphf.so - yes | pecl_module_install propro propro.so -elif [[ 1 -lt ${PECL_HTTP_VERSION%%.*} ]]; then - yes | pecl_module_install raphf-1.1.2 raphf.so - yes | pecl_module_install propro-1.0.2 propro.so -fi -printf "\n\n\n" | pecl_module_install -f pecl_http-$PECL_HTTP_VERSION http.so - -phpenv config-add ${PHP_CONFIG}