From 5dda6b1011472e558e3fd9c744c2fd537528b115 Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Mon, 3 Sep 2018 06:50:31 -0400 Subject: add CommentError, Methods::check(), and several additional tests --- src/ZipStream.php | 69 ++++++++++++++++++++------- tests/AddFileTest.php | 127 +++++++++++++++++++++++++++++++++++++++++++++----- tests/ArchiveTest.php | 63 +++++++++++++++++++++---- 3 files changed, 222 insertions(+), 37 deletions(-) diff --git a/src/ZipStream.php b/src/ZipStream.php index aa7e7e4..437ff1d 100644 --- a/src/ZipStream.php +++ b/src/ZipStream.php @@ -27,20 +27,14 @@ const VERSION = '0.3.0'; const VERSION_NEEDED = 45; /** - * Valid compression methods. + * Base class for all exceptions raised by ZipStream. */ -final class Methods { - /** Store file without compression. */ - const STORE = 0; - - /** Store file using DEFLATE compression. */ - const DEFLATE = 8; -}; +class Error extends \Exception { }; /** - * Base class for all exceptions raised by ZipStream. + * Comment length error. */ -class Error extends \Exception { }; +final class CommentError extends Error { }; /** * Deflate context error. @@ -103,6 +97,34 @@ final class PathError extends Error { } }; +/** + * Valid compression methods. + */ +final class Methods { + /** Store file without compression. */ + const STORE = 0; + + /** Store file using DEFLATE compression. */ + const DEFLATE = 8; + + /** + * Check for supported compression method. + * + * @internal + * + * @param int $method Compression method. + * + * @return void + * + * @throw UnsupportedMethodError if compression method is unsupported. + */ + static public function check(int $method) : void { + if ($method != Methods::DEFLATE && $method != Methods::STORE) { + throw new UnknownMethodError($method); + } + } +}; + /** * Abstract interface for stream output. * @@ -826,7 +848,7 @@ final class Entry { # check comment length if (strlen($comment) >= 0xFFFF) { $this->state = self::ENTRY_STATE_ERROR; - throw new Error('comment too long'); + throw new CommentError('entry comment too long'); } $this->uncompressed_size = 0; @@ -862,7 +884,7 @@ final class Entry { * * @return int Number of bytes written to output. */ - public function write(string &$data) : int { + public function write(string $data) : int { try { # check entry state if ($this->state != self::ENTRY_STATE_DATA) { @@ -1237,8 +1259,20 @@ final class ZipStream { 'time' => time(), ], $args); + # check archive method + Methods::check($this->args['method']); + + # check archive comment length + if (strlen($this->args['comment']) >= 0xFFFF) { + throw new CommentError('archive comment too long'); + } + # initialize output if (isset($args['output']) && $args['output']) { + if (!is_subclass_of($args['output'], Writer::class)) { + throw new Error('output must implement Writer interface'); + } + # use specified output writer $this->output = $args['output']; } else { @@ -1596,7 +1630,7 @@ final class ZipStream { # get comment, check length $comment = $this->args['comment']; if (strlen($comment) >= 0xFFFF) { - throw new Error('comment too long'); + throw new CommentError('archive comment too long'); } return pack('VvvvvVVv', @@ -1624,10 +1658,13 @@ final class ZipStream { */ private function get_entry_time(array &$args) : int { if (isset($args['time'])) { + # use entry time return $args['time']; } else if (isset($this->args['time'])) { + # use archive time return $this->args['time']; } else { + # fall back to current time return time(); } } @@ -1649,10 +1686,10 @@ final class ZipStream { $r = Methods::DEFLATE; } - if ($r != Methods::DEFLATE && $r != Methods::STORE) { - throw new UnknownMethodError($r); - } + # check method + Methods::check($r); + # return method return $r; } }; diff --git a/tests/AddFileTest.php b/tests/AddFileTest.php index 3ea65e0..d4b5d5a 100644 --- a/tests/AddFileTest.php +++ b/tests/AddFileTest.php @@ -7,11 +7,11 @@ use \PHPUnit\Framework\TestCase; use \Pablotron\ZipStream\ZipStream; final class AddFileTest extends BaseTestCase { - public function testCreateFile() : void { + public function testAddFile() : void { $this->with_temp_zip(function(ZipStream &$zip) { $zip->add_file('hello.txt', 'hello!'); - }, function(string $path) { - $zip = $this->open_archive($path); + }, function(string $zip_path) { + $zip = $this->open_archive($zip_path); $this->assertEquals( 'hello!', @@ -20,14 +20,57 @@ final class AddFileTest extends BaseTestCase { }); } - public function testCreateFileWithComment() : void { + public function testAddFileFromPath() : void { + $this->with_temp_zip(function(ZipStream &$zip) { + $zip->add_file_from_path('test.php', __FILE__); + }, function(string $zip_path) { + $zip = $this->open_archive($zip_path); + + $this->assertEquals( + sha1(file_get_contents(__FILE__)), + sha1($zip->getFromName('test.php')) + ); + }); + } + + public function testAddStream() : void { + $this->with_temp_zip(function(ZipStream &$zip) { + $fh = fopen(__FILE__, 'rb'); + $zip->add_stream('test.php', $fh); + fclose($fh); + }, function(string $zip_path) { + $zip = $this->open_archive($zip_path); + + $this->assertEquals( + sha1(file_get_contents(__FILE__)), + sha1($zip->getFromName('test.php')) + ); + }); + } + + public function testAdd() : void { + $this->with_temp_zip(function(ZipStream &$zip) { + $zip->add('test.php', function(&$e) { + $e->write(file_get_contents(__FILE__)); + }); + }, function(string $zip_path) { + $zip = $this->open_archive($zip_path); + + $this->assertEquals( + sha1(file_get_contents(__FILE__)), + sha1($zip->getFromName('test.php')) + ); + }); + } + + public function testAddFileWithComment() : void { $comment = 'test comment'; $this->with_temp_zip(function(ZipStream &$zip) use ($comment) { $zip->add_file('hello.txt', 'hello!', [ 'comment' => $comment, ]); - }, function(string $path) use ($comment) { - $zip = $this->open_archive($path); + }, function(string $zip_path) use ($comment) { + $zip = $this->open_archive($zip_path); $this->assertEquals( $comment, @@ -36,7 +79,7 @@ final class AddFileTest extends BaseTestCase { }); } - public function testCreateFileWithUnknownMethod() : void { + public function testAddFileWithUnknownMethod() : void { $this->expectException(\Pablotron\ZipStream\UnknownMethodError::class); $this->with_temp_zip(function(ZipStream &$zip) { @@ -46,22 +89,84 @@ final class AddFileTest extends BaseTestCase { }); } - public function testCreateFileTimestamp() : void { + public function testAddFileTimestamp() : void { # get timezone offset # $ofs = \DateTimeZone::getOffset(\DateTime::getTimezone()); - $ofs = 4 * 3600; # hard-coded to EDT for now + $ofs = 4 * 3600; # FIXME: hard-coded to EDT for now # get time from 2 hours ago (round to even number of seconds) $time = ((time() - (2 * 3600)) >> 1) << 1; + # get test time + $expected_time = $time + $ofs; + $this->with_temp_zip(function(ZipStream &$zip) use ($time) { $zip->add_file('hello.txt', 'hello!', [ 'time' => $time, ]); - }, function($zip_path) use ($time, $ofs) { + }, function($zip_path) use ($expected_time) { $zip = $this->open_archive($zip_path); $st = $zip->statName('hello.txt'); - $this->assertEquals($time, $st['mtime'] - $ofs); + + $this->assertEquals($expected_time, $st['mtime']); + }); + } + + public function testAddFileCRC() : void { + $data = 'hello!'; + + # calculate crc32b of file data + $hash = hash('crc32b', $data, true); + + # pack expected crc as integer + $expected_crc = ( + (ord($hash[0]) << 24) | + (ord($hash[1]) << 16) | + (ord($hash[2]) << 8) | + (ord($hash[3])) + ); + + $this->with_temp_zip(function(ZipStream &$zip) use ($data) { + $zip->add_file('hello.txt', $data); + }, function($zip_path) use ($expected_crc) { + $zip = $this->open_archive($zip_path); + $st = $zip->statName('hello.txt'); + + $this->assertEquals($expected_crc, $st['crc']); + }); + } + + public function testAddFileWithMethodStore() : void { + $data = file_get_contents(__FILE__); + + $this->with_temp_zip(function(ZipStream &$zip) use ($data) { + $zip->add_file('test.php', $data, [ + 'method' => \Pablotron\ZipStream\Methods::STORE, + ]); + }, function($zip_path) use ($data) { + $zip = $this->open_archive($zip_path); + + $this->assertEquals( + sha1($data), + sha1($zip->getFromName('test.php')) + ); + }); + } + + public function testAddFileWithMethodDeflate() : void { + $data = file_get_contents(__FILE__); + + $this->with_temp_zip(function(ZipStream &$zip) use ($data) { + $zip->add_file('test.php', $data, [ + 'method' => \Pablotron\ZipStream\Methods::DEFLATE, + ]); + }, function($zip_path) use ($data) { + $zip = $this->open_archive($zip_path); + + $this->assertEquals( + sha1($data), + sha1($zip->getFromName('test.php')) + ); }); } }; diff --git a/tests/ArchiveTest.php b/tests/ArchiveTest.php index cbac0f1..8345ee4 100644 --- a/tests/ArchiveTest.php +++ b/tests/ArchiveTest.php @@ -19,15 +19,15 @@ final class ArchiveTest extends BaseTestCase { } public function testFileWriter() { - $this->with_temp_file(function(string $dst_path) { - ZipStream::send($dst_path, function(ZipStream &$zip) { + $this->with_temp_file(function(string $zip_path) { + ZipStream::send($zip_path, function(ZipStream &$zip) { $zip->add_file('hello.txt', 'hello!'); }, [ 'output' => new FileWriter(), ]); # open archive - $zip = $this->open_archive($dst_path); + $zip = $this->open_archive($zip_path); # read hello.txt, check text $this->assertEquals( @@ -38,13 +38,13 @@ final class ArchiveTest extends BaseTestCase { } public function testStreamWriter() { - $this->with_temp_file(function(string $dst_path) { - $fh = fopen($dst_path, 'wb'); + $this->with_temp_file(function(string $zip_path) { + $fh = fopen($zip_path, 'wb'); if ($fh === false) { throw new Exception("fopen() failed"); } - ZipStream::send($dst_path, function(ZipStream &$zip) { + ZipStream::send($zip_path, function(ZipStream &$zip) { $zip->add_file('hello.txt', 'hello!'); }, [ 'output' => new StreamWriter($fh), @@ -54,7 +54,7 @@ final class ArchiveTest extends BaseTestCase { fclose($fh); # open archive - $zip = $this->open_archive($dst_path); + $zip = $this->open_archive($zip_path); # read hello.txt, check text $this->assertEquals( @@ -65,11 +65,11 @@ final class ArchiveTest extends BaseTestCase { } public function testArchiveComment() : void { - $this->with_temp_file(function($dst_path) { + $this->with_temp_file(function($zip_path) { $comment = 'test archive comment'; # write archive - ZipStream::send($dst_path, function(ZipStream &$zip) { + ZipStream::send($zip_path, function(ZipStream &$zip) { $zip->add_file('hello.txt', 'hello!'); }, [ 'comment' => $comment, @@ -77,7 +77,7 @@ final class ArchiveTest extends BaseTestCase { ]); # open archive - $zip = $this->open_archive($dst_path); + $zip = $this->open_archive($zip_path); # read hello.txt, check text $this->assertEquals( @@ -86,4 +86,47 @@ final class ArchiveTest extends BaseTestCase { ); }); } + + public function testLongArchiveComment() : void { + $this->expectException(\Pablotron\ZipStream\CommentError::class); + + $this->with_temp_file(function($zip_path) { + $comment = str_repeat('x', 0xFFFF); + + # write archive + ZipStream::send($zip_path, function(ZipStream &$zip) { + $zip->add_file('hello.txt', 'hello!'); + }, [ + 'comment' => $comment, + 'output' => new FileWriter(), + ]); + }); + } + + public function testInvalidOutput() : void { + $this->expectException(\Pablotron\ZipStream\Error::class); + + $this->with_temp_file(function($zip_path) { + # write archive with invalid writer + ZipStream::send($zip_path, function(ZipStream &$zip) { + $zip->add_file('hello.txt', 'hello!'); + }, [ + 'output' => 'bad writer', + ]); + }); + } + + public function testInvalidMethod() : void { + $this->expectException(\Pablotron\ZipStream\UnknownMethodError::class); + + $this->with_temp_file(function($zip_path) { + # write archive with invalid compression method + ZipStream::send($zip_path, function(ZipStream &$zip) { + $zip->add_file('hello.txt', 'hello!'); + }, [ + 'method' => 100, + 'output' => new FileWriter(), + ]); + }); + } }; -- cgit v1.2.3