Skip to content

Commit 8df04fa

Browse files
authored
Merge pull request #28 from clue-labs/streaming
Change Compressor and Decompressor to use more efficient streaming compression context (requires PHP 7+)
2 parents 177677f + ef81edf commit 8df04fa

12 files changed

+145
-179
lines changed

.travis.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ dist: trusty
55

66
matrix:
77
include:
8-
- php: 5.4
9-
- php: 5.5
10-
- php: 5.6
118
- php: 7.0
129
- php: 7.1
1310
- php: 7.2

README.md

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ supporting compression and decompression of GZIP, ZLIB and raw DEFLATE formats.
1515
* [Usage](#usage)
1616
* [Compressor](#compressor)
1717
* [Decompressor](#decompressor)
18-
* [Inconsistencies](#inconsistencies)
1918
* [Install](#install)
2019
* [Tests](#tests)
2120
* [License](#license)
@@ -162,17 +161,6 @@ $input->pipe($decompressor)->pipe($filterBadWords)->pipe($output);
162161
For more details, see ReactPHP's
163162
[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface).
164163

165-
### Inconsistencies
166-
167-
The stream compression filters are not exactly the most commonly used features of PHP.
168-
As such, we've spotted some inconsistencies (or *bugs*) in different PHP versions.
169-
These inconsistencies exist in the underlying PHP engines and there's little we can do about this in this library.
170-
171-
* All PHP versions: Decompressing invalid data does not emit any data (and does not raise an error)
172-
173-
Our test suite contains several test cases that exhibit these issues.
174-
If you feel some test case is missing or outdated, we're happy to accept PRs! :)
175-
176164
## Install
177165

178166
The recommended way to install this library is [through Composer](https://getcomposer.org).
@@ -188,9 +176,7 @@ $ composer require clue/zlib-react:^0.2.2
188176
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
189177

190178
This project aims to run on any platform and thus does not require any PHP
191-
extensions besides `ext-zlib` and supports running on legacy PHP 5.4 through current
192-
PHP 7+.
193-
It's *highly recommended to use PHP 7+* for this project.
179+
extensions besides `ext-zlib` and supports running on current PHP 7+.
194180

195181
The `ext-zlib` extension is required for handling the underlying data compression
196182
and decompression.
@@ -200,8 +186,8 @@ builds by default. If you're building PHP from source, you may have to
200186
[manually enable](https://www.php.net/manual/en/zlib.installation.php) it.
201187

202188
We're committed to providing a smooth upgrade path for legacy setups.
203-
If you need to support legacy PHP 5.3 and legacy HHVM, you may want to check out
204-
the legacy `v0.2.x` release branch.
189+
If you need to support legacy PHP versions and legacy HHVM, you may want to
190+
check out the legacy `v0.2.x` release branch.
205191
This legacy release branch also provides an installation candidate that does not
206192
require `ext-zlib` during installation but uses runtime checks instead.
207193

composer.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111
}
1212
],
1313
"require": {
14-
"php": ">=5.4",
14+
"php": ">=7.0",
1515
"ext-zlib": "*",
16-
"clue/stream-filter": "~1.3",
1716
"react/stream": "^1.0 || ^0.7 || ^0.6"
1817
},
1918
"require-dev": {

src/Compressor.php

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,44 @@
3131
* For more details, see ReactPHP's
3232
* [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface).
3333
*/
34-
final class Compressor extends ZlibFilterStream
34+
final class Compressor extends TransformStream
3535
{
36+
/** @var ?resource */
37+
private $context;
38+
3639
/**
3740
* @param int $encoding ZLIB_ENCODING_GZIP, ZLIB_ENCODING_RAW or ZLIB_ENCODING_DEFLATE
3841
* @param int $level optional compression level
3942
*/
4043
public function __construct($encoding, $level = -1)
4144
{
42-
parent::__construct(
43-
Filter\fun('zlib.deflate', array('window' => $encoding, 'level' => $level))
44-
);
45+
$context = @deflate_init($encoding, ['level' => $level]);
46+
if ($context === false) {
47+
throw new \InvalidArgumentException('Unable to initialize compressor' . strstr(error_get_last()['message'], ':'));
48+
}
49+
50+
$this->context = $context;
51+
}
52+
53+
protected function transformData($chunk)
54+
{
55+
$ret = deflate_add($this->context, $chunk, ZLIB_NO_FLUSH);
56+
57+
if ($ret !== '') {
58+
$this->emit('data', [$ret]);
59+
}
60+
}
61+
62+
protected function transformEnd($chunk)
63+
{
64+
$ret = deflate_add($this->context, $chunk, ZLIB_FINISH);
65+
$this->context = null;
66+
67+
if ($ret !== '') {
68+
$this->emit('data', [$ret]);
69+
}
4570

46-
$this->emptyWrite = $encoding;
71+
$this->emit('end');
72+
$this->close();
4773
}
4874
}

src/Decompressor.php

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,50 @@
3131
* For more details, see ReactPHP's
3232
* [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface).
3333
*/
34-
final class Decompressor extends ZlibFilterStream
34+
final class Decompressor extends TransformStream
3535
{
36+
/** @var ?resource */
37+
private $context;
38+
3639
/**
3740
* @param int $encoding ZLIB_ENCODING_GZIP, ZLIB_ENCODING_RAW or ZLIB_ENCODING_DEFLATE
3841
*/
3942
public function __construct($encoding)
4043
{
41-
parent::__construct(
42-
Filter\fun('zlib.inflate', array('window' => $encoding))
43-
);
44+
$context = @inflate_init($encoding);
45+
if ($context === false) {
46+
throw new \InvalidArgumentException('Unable to initialize decompressor' . strstr(error_get_last()['message'], ':'));
47+
}
48+
49+
$this->context = $context;
50+
}
51+
52+
protected function transformData($chunk)
53+
{
54+
$ret = @inflate_add($this->context, $chunk);
55+
if ($ret === false) {
56+
throw new \RuntimeException('Unable to decompress' . strstr(error_get_last()['message'], ':'));
57+
}
58+
59+
if ($ret !== '') {
60+
$this->emit('data', [$ret]);
61+
}
62+
}
63+
64+
protected function transformEnd($chunk)
65+
{
66+
$ret = @inflate_add($this->context, $chunk, ZLIB_FINISH);
67+
$this->context = null;
68+
69+
if ($ret === false) {
70+
throw new \RuntimeException('Unable to decompress' . strstr(error_get_last()['message'], ':'));
71+
}
72+
73+
if ($ret !== '') {
74+
$this->emit('data', [$ret]);
75+
}
76+
77+
$this->emit('end');
78+
$this->close();
4479
}
4580
}

src/TransformStream.php

Lines changed: 19 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public function write($data)
3535

3636
return true;
3737
} catch (Exception $e) {
38-
$this->forwardError($e);
38+
$this->emit('error', [$e]);
39+
$this->close();
3940
return false;
4041
}
4142
}
@@ -53,7 +54,8 @@ public function end($data = null)
5354
}
5455
$this->transformEnd($data);
5556
} catch (Exception $e) {
56-
$this->forwardError($e);
57+
$this->emit('error', [$e]);
58+
$this->close();
5759
}
5860
}
5961

@@ -107,109 +109,49 @@ public function pipe(WritableStreamInterface $dest, array $options = array())
107109
return $dest;
108110
}
109111

110-
/**
111-
* Forwards a single "data" event to the reading side of the stream
112-
*
113-
* This will emit an "data" event.
114-
*
115-
* If the stream is not readable, then this is a NO-OP.
116-
*
117-
* @param string $data
118-
*/
119-
protected function forwardData($data)
120-
{
121-
if (!$this->readable) {
122-
return;
123-
}
124-
$this->emit('data', array($data));
125-
}
126-
127-
/**
128-
* Forwards an "end" event to the reading side of the stream
129-
*
130-
* This will emit an "end" event and will then close this stream.
131-
*
132-
* If the stream is not readable, then this is a NO-OP.
133-
*
134-
* @uses self::close()
135-
*/
136-
protected function forwardEnd()
137-
{
138-
if (!$this->readable) {
139-
return;
140-
}
141-
$this->readable = false;
142-
$this->writable = false;
143-
144-
$this->emit('end');
145-
$this->close();
146-
}
147-
148-
/**
149-
* Forwards the given $error message to the reading side of the stream
150-
*
151-
* This will emit an "error" event and will then close this stream.
152-
*
153-
* If the stream is not readable, then this is a NO-OP.
154-
*
155-
* @param Exception $error
156-
* @uses self::close()
157-
*/
158-
protected function forwardError(Exception $error)
159-
{
160-
if (!$this->readable) {
161-
return;
162-
}
163-
$this->readable = false;
164-
$this->writable = false;
165-
166-
$this->emit('error', array($error));
167-
$this->close();
168-
}
169-
170112
/**
171113
* can be overwritten in order to implement custom transformation behavior
172114
*
173-
* This gets passed a single chunk of $data and should invoke `forwardData()`
115+
* This gets passed a single chunk of $data and should emit a `data` event
174116
* with the filtered result.
175117
*
176-
* If the given data chunk is not valid, then you should invoke `forwardError()`
177-
* or throw an Exception.
118+
* If the given data chunk is not valid, then you should throw an Exception
119+
* which will automatically be turned into an `error` event.
178120
*
179-
* If you do not overwrite this method, then its default implementation simply
180-
* invokes `forwardData()` on the unmodified input data chunk.
121+
* If you do not overwrite this method, then its default implementation
122+
* simply emits a `data` event with the unmodified input data chunk.
181123
*
182124
* @param string $data
183-
* @see self::forwardData()
184125
*/
185126
protected function transformData($data)
186127
{
187-
$this->forwardData($data);
128+
$this->emit('data', [$data]);
188129
}
189130

190131
/**
191132
* can be overwritten in order to implement custom stream ending behavior
192133
*
193-
* This may get passed a single final chunk of $data and should invoke `forwardEnd()`.
134+
* This may get passed a single final chunk of $data and should emit an
135+
* `end` event and close the stream.
194136
*
195-
* If the given data chunk is not valid, then you should invoke `forwardError()`
196-
* or throw an Exception.
137+
* If the given data chunk is not valid, then you should throw an Exception
138+
* which will automatically be turned into an `error` event.
197139
*
198140
* If you do not overwrite this method, then its default implementation simply
199141
* invokes `transformData()` on the unmodified input data chunk (if any),
200-
* which in turn defaults to invoking `forwardData()` and then finally
201-
* invokes `forwardEnd()`.
142+
* which in turn defaults to emitting a `data` event and then finally
143+
* emits an `end` event and closes the stream.
202144
*
203145
* @param string $data
204146
* @see self::transformData()
205-
* @see self::forwardData()
206-
* @see self::forwardEnd()
207147
*/
208148
protected function transformEnd($data)
209149
{
210150
if ($data !== '') {
211151
$this->transformData($data);
212152
}
213-
$this->forwardEnd();
153+
154+
$this->emit('end');
155+
$this->close();
214156
}
215157
}

src/ZlibFilterStream.php

Lines changed: 0 additions & 65 deletions
This file was deleted.

tests/CompressorTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
use Clue\React\Zlib\Compressor;
4+
5+
class CompressorTest extends TestCase
6+
{
7+
/**
8+
* @expectedException InvalidArgumentException
9+
*/
10+
public function testCtorThrowsForInvalidEncoding()
11+
{
12+
new Compressor(0);
13+
}
14+
}

0 commit comments

Comments
 (0)