diff --git a/src/Decompressor.php b/src/Decompressor.php index ae17b26..e10f36d 100644 --- a/src/Decompressor.php +++ b/src/Decompressor.php @@ -34,6 +34,9 @@ final class Decompressor extends TransformStream /** @var ?resource */ private $context; + /** @var bool */ + private $incomplete = false; + /** * @param int $encoding ZLIB_ENCODING_GZIP, ZLIB_ENCODING_RAW or ZLIB_ENCODING_DEFLATE */ @@ -80,6 +83,9 @@ protected function transformData($chunk) throw new \RuntimeException('Unable to decompress' . $errstr); } + // detect incomplete data on PHP 7.2+ only, older versions lack inflate_get_status() + $this->incomplete = PHP_VERSION_ID >= 70200 && inflate_get_status($this->context) !== ZLIB_STREAM_END; + if ($ret !== '') { $this->emit('data', [$ret]); } @@ -87,24 +93,14 @@ protected function transformData($chunk) protected function transformEnd($chunk) { - $errstr = ''; - set_error_handler(function ($_, $error) use (&$errstr) { - // Match errstr from PHP's warning message. - // inflate_add(): data error - $errstr = strstr($error, ':'); - }); + if ($chunk !== '') { + $this->transformData($chunk); + } - $ret = inflate_add($this->context, $chunk, ZLIB_FINISH); $this->context = null; - restore_error_handler(); - - if ($ret === false) { - throw new \RuntimeException('Unable to decompress' . $errstr); - } - - if ($ret !== '') { - $this->emit('data', [$ret]); + if ($this->incomplete) { + throw new \RuntimeException('Unable to decompress: unexpected end of stream'); // @codeCoverageIgnore } $this->emit('end'); diff --git a/tests/DeflateDecompressorTest.php b/tests/DeflateDecompressorTest.php index cbd6906..f11b33b 100644 --- a/tests/DeflateDecompressorTest.php +++ b/tests/DeflateDecompressorTest.php @@ -24,6 +24,14 @@ public function testInflateEmpty() $this->decompressor->end(gzdeflate('')); } + public function testInflateWithoutDataEmitsEnd() + { + $this->decompressor->on('error', $this->expectCallableNever()); + $this->decompressor->on('end', $this->expectCallableOnce()); + + $this->decompressor->end(); + } + public function testInflateHelloWorld() { $this->decompressor->on('data', function ($data) use (&$buffered) { @@ -84,4 +92,17 @@ public function testDecompressInvalidOnEndEmitsErrorWithoutCallingCustomErrorHan restore_error_handler(); $this->assertNull($error); } + + public function testDecompressTruncatedDataOnEndEmitsError() + { + if (PHP_VERSION_ID < 70200) { + $this->markTestSkipped('Requires PHP 7.2+ to detect incomplete data'); + } + + $this->decompressor->on('end', $this->expectCallableNever()); + $this->decompressor->on('error', $this->expectCallableOnce()); + + $this->decompressor->write(substr(gzdeflate('hello world'), 0, -1)); + $this->decompressor->end(); + } } diff --git a/tests/GzipDecompressorTest.php b/tests/GzipDecompressorTest.php index 2f8fabd..005a6b4 100644 --- a/tests/GzipDecompressorTest.php +++ b/tests/GzipDecompressorTest.php @@ -24,6 +24,14 @@ public function testDecompressEmpty() $this->decompressor->end(gzencode('')); } + public function testDecompressWithoutDataEmitsEnd() + { + $this->decompressor->on('error', $this->expectCallableNever()); + $this->decompressor->on('end', $this->expectCallableOnce()); + + $this->decompressor->end(); + } + public function testDecompressHelloWorld() { $this->decompressor->on('data', function ($data) use (&$buffered) { @@ -84,4 +92,17 @@ public function testDecompressInvalidOnEndEmitsErrorWithoutCallingCustomErrorHan restore_error_handler(); $this->assertNull($error); } + + public function testDecompressTruncatedDataOnEndEmitsError() + { + if (PHP_VERSION_ID < 70200) { + $this->markTestSkipped('Requires PHP 7.2+ to detect incomplete data'); + } + + $this->decompressor->on('end', $this->expectCallableNever()); + $this->decompressor->on('error', $this->expectCallableOnce()); + + $this->decompressor->write(substr(gzencode('hello world'), 0, -1)); + $this->decompressor->end(); + } } diff --git a/tests/ZlibDecompressorTest.php b/tests/ZlibDecompressorTest.php index 15b29d8..90d09eb 100644 --- a/tests/ZlibDecompressorTest.php +++ b/tests/ZlibDecompressorTest.php @@ -24,6 +24,14 @@ public function testDecompressEmpty() $this->decompressor->end(gzcompress('')); } + public function testDecompressWithoutDataEmitsEnd() + { + $this->decompressor->on('error', $this->expectCallableNever()); + $this->decompressor->on('end', $this->expectCallableOnce()); + + $this->decompressor->end(); + } + public function testDecompressHelloWorld() { $this->decompressor->on('data', function ($data) use (&$buffered) { @@ -84,4 +92,17 @@ public function testDecompressInvalidOnEndEmitsErrorWithoutCallingCustomErrorHan restore_error_handler(); $this->assertNull($error); } + + public function testDecompressTruncatedDataOnEndEmitsError() + { + if (PHP_VERSION_ID < 70200) { + $this->markTestSkipped('Requires PHP 7.2+ to detect incomplete data'); + } + + $this->decompressor->on('end', $this->expectCallableNever()); + $this->decompressor->on('error', $this->expectCallableOnce()); + + $this->decompressor->write(substr(gzcompress('hello world'), 0, -1)); + $this->decompressor->end(); + } }