From 2f969fb89ce9ffc640cc9d5238df84a4841a159d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 18 Oct 2015 18:29:31 +0200 Subject: [PATCH 1/7] Support accessing built-in filters as callbacks --- src/functions.php | 35 +++++++++++++++++++++++++++++++++++ tests/FilterTest.php | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/functions.php b/src/functions.php index bc91d0e..1c832f4 100644 --- a/src/functions.php +++ b/src/functions.php @@ -48,6 +48,41 @@ function prepend($stream, $callback, $read_write = STREAM_FILTER_ALL) return $ret; } +/** + * Creates a filter callback which uses the given built-in $filter + * + * @param string $filter built-in filter name, see stream_get_filters() + * @param mixed $params additional parameters to pass to the built-in filter + * @return callable a filter callback which can be append()'ed or prepend()'ed + * @throws RuntimeException on error + * @see stream_get_filters() + * @see append() + */ +function builtin($filter, $params = null) +{ + $fp = fopen('php://memory', 'r+'); + $ret = @stream_filter_append($fp, $filter, STREAM_FILTER_WRITE, $params); + + if ($ret === false) { + fclose($fp); + $error = error_get_last() + array('message' => ''); + throw new RuntimeException('Unable to access built-in filter: ' . $error['message']); + } + + $buffer = ''; + append($fp, function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }, STREAM_FILTER_WRITE); + + return function ($chunk) use ($fp, &$buffer) { + $buffer = ''; + + fwrite($fp, $chunk); + + return $buffer; + }; +} + /** * remove a callback filter from the given stream * diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 0af76df..c0c0857 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -159,6 +159,38 @@ public function testRemoveFilter() fclose($stream); } + public function testBuiltInRot13() + { + $rot = StreamFilter\builtin('string.rot13'); + + $this->assertEquals('grfg', $rot('test')); + $this->assertEquals('test', $rot($rot('test'))); + } + + public function testAppendBuiltInDechunk() + { + $stream = $this->createStream(); + + StreamFilter\append($stream, StreamFilter\builtin('dechunk'), STREAM_FILTER_WRITE); + + fwrite($stream, "2\r\nhe\r\n"); + fwrite($stream, "3\r\nllo\r\n"); + fwrite($stream, "0\r\n"); + rewind($stream); + + $this->assertEquals('hello', stream_get_contents($stream)); + + fclose($stream); + } + + /** + * @expectedException RuntimeException + */ + public function testBuildInInvalid() + { + StreamFilter\builtin('unknown'); + } + /** * @expectedException RuntimeException */ From 10e00e6b6a31328364fc8b878f3a0bf4998ac49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 19 Oct 2015 01:24:29 +0200 Subject: [PATCH 2/7] Improve strictness and interoperability --- src/functions.php | 7 ++++++- tests/FilterTest.php | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/functions.php b/src/functions.php index 1c832f4..d234856 100644 --- a/src/functions.php +++ b/src/functions.php @@ -69,16 +69,21 @@ function builtin($filter, $params = null) throw new RuntimeException('Unable to access built-in filter: ' . $error['message']); } + // append filter function which buffers internally $buffer = ''; append($fp, function ($chunk) use (&$buffer) { $buffer .= $chunk; + + // always return empty string in order to skip actually writing to stream resource + return ''; }, STREAM_FILTER_WRITE); return function ($chunk) use ($fp, &$buffer) { + // initialize buffer and invoke filters by attempting to write to stream $buffer = ''; - fwrite($fp, $chunk); + // buffer now contains everything the filter function returned return $buffer; }; } diff --git a/tests/FilterTest.php b/tests/FilterTest.php index c0c0857..2d0e6aa 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -169,13 +169,20 @@ public function testBuiltInRot13() public function testAppendBuiltInDechunk() { + try { + $dechunk = StreamFilter\builtin('dechunk'); + } catch (Exception $e) { + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); + throw $e; + } + $stream = $this->createStream(); - StreamFilter\append($stream, StreamFilter\builtin('dechunk'), STREAM_FILTER_WRITE); + StreamFilter\append($stream, $dechunk, STREAM_FILTER_WRITE); fwrite($stream, "2\r\nhe\r\n"); fwrite($stream, "3\r\nllo\r\n"); - fwrite($stream, "0\r\n"); + fwrite($stream, "0\r\n\r\n"); rewind($stream); $this->assertEquals('hello', stream_get_contents($stream)); @@ -196,8 +203,8 @@ public function testBuildInInvalid() */ public function testAppendInvalidStreamIsRuntimeError() { - if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); StreamFilter\append(false, function () { }); + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); } /** @@ -205,8 +212,8 @@ public function testAppendInvalidStreamIsRuntimeError() */ public function testPrependInvalidStreamIsRuntimeError() { - if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); StreamFilter\prepend(false, function () { }); + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); } /** @@ -214,8 +221,8 @@ public function testPrependInvalidStreamIsRuntimeError() */ public function testRemoveInvalidFilterIsRuntimeError() { - if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); StreamFilter\remove(false); + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); } /** From 29d1993dbcdf97964a95fd893cccfe798c038795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 19 Oct 2015 22:34:19 +0200 Subject: [PATCH 3/7] Close (flush) built-in filters --- src/functions.php | 19 ++++++++++++---- tests/BuiltInZlibTest.php | 48 +++++++++++++++++++++++++++++++++++++++ tests/FilterTest.php | 12 ++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 tests/BuiltInZlibTest.php diff --git a/src/functions.php b/src/functions.php index d234856..982cf21 100644 --- a/src/functions.php +++ b/src/functions.php @@ -60,10 +60,10 @@ function prepend($stream, $callback, $read_write = STREAM_FILTER_ALL) */ function builtin($filter, $params = null) { - $fp = fopen('php://memory', 'r+'); - $ret = @stream_filter_append($fp, $filter, STREAM_FILTER_WRITE, $params); + $fp = fopen('php://memory', 'w'); + $filter = @stream_filter_append($fp, $filter, STREAM_FILTER_WRITE, $params); - if ($ret === false) { + if ($filter === false) { fclose($fp); $error = error_get_last() + array('message' => ''); throw new RuntimeException('Unable to access built-in filter: ' . $error['message']); @@ -78,7 +78,18 @@ function builtin($filter, $params = null) return ''; }, STREAM_FILTER_WRITE); - return function ($chunk) use ($fp, &$buffer) { + $closed = false; + + return function ($chunk = null) use ($fp, $filter, &$buffer, &$closed) { + if ($closed) { + throw new \RuntimeException('Unable to perform operation on closed stream'); + } + if ($chunk === null) { + $closed = true; + $buffer = ''; + fclose($fp); + return $buffer; + } // initialize buffer and invoke filters by attempting to write to stream $buffer = ''; fwrite($fp, $chunk); diff --git a/tests/BuiltInZlibTest.php b/tests/BuiltInZlibTest.php new file mode 100644 index 0000000..b28563a --- /dev/null +++ b/tests/BuiltInZlibTest.php @@ -0,0 +1,48 @@ +assertEquals("\x03\x00", $data); + } + + public function testBuiltInZlibDeflateBig() + { + $deflate = StreamFilter\builtin('zlib.deflate'); + + $n = 1000; + $expected = str_repeat('hello', $n); + + $bytes = ''; + for ($i = 0; $i < $n; ++$i) { + $bytes .= $deflate('hello'); + } + $bytes .= $deflate(); + + $this->assertEquals($expected, gzinflate($bytes)); + } + + public function testBuiltInZlibInflateBig() + { + $inflate = StreamFilter\builtin('zlib.inflate'); + + $expected = str_repeat('hello', 10); + $bytes = gzdeflate($expected); + + $ret = ''; + foreach (str_split($bytes, 2) as $chunk) { + $ret .= $inflate($chunk); + } + $ret .= $inflate(); + + $this->assertEquals($expected, $ret); + } +} diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 9a7460a..51b4ac5 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -227,6 +227,18 @@ public function testBuiltInRot13() $this->assertEquals('grfg', $rot('test')); $this->assertEquals('test', $rot($rot('test'))); + $this->assertEquals(null, $rot()); + } + + /** + * @expectedException RuntimeException + */ + public function testBuiltInWriteAfterCloseRot13() + { + $rot = StreamFilter\builtin('string.rot13'); + + $this->assertEquals(null, $rot()); + $rot('test'); } public function testAppendBuiltInDechunk() From 80cbdeb85a3cbd193a98b9907fb9d24c84cea65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 7 Nov 2015 19:14:14 +0100 Subject: [PATCH 4/7] Rename builtin() to fun() --- src/functions.php | 4 +-- tests/FilterTest.php | 32 ++--------------- tests/FunTest.php | 34 +++++++++++++++++++ .../{BuiltInZlibTest.php => FunZlibTest.php} | 12 +++---- 4 files changed, 44 insertions(+), 38 deletions(-) create mode 100644 tests/FunTest.php rename tests/{BuiltInZlibTest.php => FunZlibTest.php} (71%) diff --git a/src/functions.php b/src/functions.php index 982cf21..6c8125b 100644 --- a/src/functions.php +++ b/src/functions.php @@ -49,7 +49,7 @@ function prepend($stream, $callback, $read_write = STREAM_FILTER_ALL) } /** - * Creates a filter callback which uses the given built-in $filter + * Creates filter fun (function) which uses the given built-in $filter * * @param string $filter built-in filter name, see stream_get_filters() * @param mixed $params additional parameters to pass to the built-in filter @@ -58,7 +58,7 @@ function prepend($stream, $callback, $read_write = STREAM_FILTER_ALL) * @see stream_get_filters() * @see append() */ -function builtin($filter, $params = null) +function fun($filter, $params = null) { $fp = fopen('php://memory', 'w'); $filter = @stream_filter_append($fp, $filter, STREAM_FILTER_WRITE, $params); diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 51b4ac5..700c0a2 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -221,30 +221,10 @@ public function testRemoveFilter() fclose($stream); } - public function testBuiltInRot13() - { - $rot = StreamFilter\builtin('string.rot13'); - - $this->assertEquals('grfg', $rot('test')); - $this->assertEquals('test', $rot($rot('test'))); - $this->assertEquals(null, $rot()); - } - - /** - * @expectedException RuntimeException - */ - public function testBuiltInWriteAfterCloseRot13() - { - $rot = StreamFilter\builtin('string.rot13'); - - $this->assertEquals(null, $rot()); - $rot('test'); - } - - public function testAppendBuiltInDechunk() + public function testAppendFunDechunk() { try { - $dechunk = StreamFilter\builtin('dechunk'); + $dechunk = StreamFilter\fun('dechunk'); } catch (Exception $e) { if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); throw $e; @@ -264,14 +244,6 @@ public function testAppendBuiltInDechunk() fclose($stream); } - /** - * @expectedException RuntimeException - */ - public function testBuildInInvalid() - { - StreamFilter\builtin('unknown'); - } - public function testAppendThrows() { $this->createErrorHandler($errors); diff --git a/tests/FunTest.php b/tests/FunTest.php new file mode 100644 index 0000000..2eb1dd9 --- /dev/null +++ b/tests/FunTest.php @@ -0,0 +1,34 @@ +assertEquals('grfg', $rot('test')); + $this->assertEquals('test', $rot($rot('test'))); + $this->assertEquals(null, $rot()); + } + + /** + * @expectedException RuntimeException + */ + public function testFunWriteAfterCloseRot13() + { + $rot = Filter\fun('string.rot13'); + + $this->assertEquals(null, $rot()); + $rot('test'); + } + + /** + * @expectedException RuntimeException + */ + public function testFunInvalid() + { + Filter\fun('unknown'); + } +} diff --git a/tests/BuiltInZlibTest.php b/tests/FunZlibTest.php similarity index 71% rename from tests/BuiltInZlibTest.php rename to tests/FunZlibTest.php index b28563a..ac2b423 100644 --- a/tests/BuiltInZlibTest.php +++ b/tests/FunZlibTest.php @@ -4,9 +4,9 @@ class BuiltInZlibTest extends PHPUnit_Framework_TestCase { - public function testBuiltInZlibDeflateEmpty() + public function testFunZlibDeflateEmpty() { - $deflate = StreamFilter\builtin('zlib.deflate'); + $deflate = StreamFilter\fun('zlib.deflate'); //$data = gzdeflate(''); $data = $deflate(); @@ -14,9 +14,9 @@ public function testBuiltInZlibDeflateEmpty() $this->assertEquals("\x03\x00", $data); } - public function testBuiltInZlibDeflateBig() + public function testFunZlibDeflateBig() { - $deflate = StreamFilter\builtin('zlib.deflate'); + $deflate = StreamFilter\fun('zlib.deflate'); $n = 1000; $expected = str_repeat('hello', $n); @@ -30,9 +30,9 @@ public function testBuiltInZlibDeflateBig() $this->assertEquals($expected, gzinflate($bytes)); } - public function testBuiltInZlibInflateBig() + public function testFunZlibInflateBig() { - $inflate = StreamFilter\builtin('zlib.inflate'); + $inflate = StreamFilter\fun('zlib.inflate'); $expected = str_repeat('hello', 10); $bytes = gzdeflate($expected); From 7cd7e4c1994b48dfbdcd59ac4d27530926a2c5ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 7 Nov 2015 19:21:39 +0100 Subject: [PATCH 5/7] Skip all tests that are known to fail due to engine inconsistencies --- tests/FilterTest.php | 19 +++++++------------ tests/FunZlibTest.php | 4 ++++ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 700c0a2..02aa3a4 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -223,16 +223,11 @@ public function testRemoveFilter() public function testAppendFunDechunk() { - try { - $dechunk = StreamFilter\fun('dechunk'); - } catch (Exception $e) { - if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); - throw $e; - } + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (dechunk filter does not exist)'); $stream = $this->createStream(); - StreamFilter\append($stream, $dechunk, STREAM_FILTER_WRITE); + StreamFilter\append($stream, StreamFilter\fun('dechunk'), STREAM_FILTER_WRITE); fwrite($stream, "2\r\nhe\r\n"); fwrite($stream, "3\r\nllo\r\n"); @@ -280,8 +275,8 @@ public function testAppendThrowsDuringEnd() // We can only assert we're not seeing an exception hereā€¦ // * php 5.3-5.6 sees one error here - // * php 7 does not any error here - // * hhvm seems the same error twice + // * php 7 does not see any error here + // * hhvm sees the same error twice // // If you're curious: // @@ -339,8 +334,8 @@ public function testAppendThrowsShouldTriggerEndButIgnoreExceptionDuringEnd() */ public function testAppendInvalidStreamIsRuntimeError() { + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (does not reject invalid stream)'); StreamFilter\append(false, function () { }); - if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); } /** @@ -348,8 +343,8 @@ public function testAppendInvalidStreamIsRuntimeError() */ public function testPrependInvalidStreamIsRuntimeError() { + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (does not reject invalid stream)'); StreamFilter\prepend(false, function () { }); - if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); } /** @@ -357,8 +352,8 @@ public function testPrependInvalidStreamIsRuntimeError() */ public function testRemoveInvalidFilterIsRuntimeError() { + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (does not reject invalid filters)'); StreamFilter\remove(false); - if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM'); } /** diff --git a/tests/FunZlibTest.php b/tests/FunZlibTest.php index ac2b423..1ca2726 100644 --- a/tests/FunZlibTest.php +++ b/tests/FunZlibTest.php @@ -6,6 +6,8 @@ class BuiltInZlibTest extends PHPUnit_Framework_TestCase { public function testFunZlibDeflateEmpty() { + if (PHP_VERSION >= 7) $this->markTestSkipped('Not supported on PHP7 (empty string does not invoke filter)'); + $deflate = StreamFilter\fun('zlib.deflate'); //$data = gzdeflate(''); @@ -32,6 +34,8 @@ public function testFunZlibDeflateBig() public function testFunZlibInflateBig() { + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (final chunk will not be emitted)'); + $inflate = StreamFilter\fun('zlib.inflate'); $expected = str_repeat('hello', 10); From f5ee60ebd046b37f6ba37d275d6f65acfcfe179d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 9 Nov 2015 00:21:34 +0100 Subject: [PATCH 6/7] Documentation for fun() --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/README.md b/README.md index 231a5e2..e37bc71 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,75 @@ $filter = Filter\prepend($stream, function ($chunk) { }); ``` +### fun() + +The `fun($filter, $parameters = null)` function can be used to +create a filter function which uses the given built-in `$filter`. + +PHP comes with a useful set of [built-in filters](http://php.net/manual/en/filters.php). +Using `fun()` makes accessing these as easy as passing an input string to filter +and getting the filtered output string. + +```php +$fun = Filter\fun('string.rot13'); + +assert('grfg' === $fun('test')); +assert('test' === $fun($fun('test')); +``` + +Please note that not all filter functions may be available depending on installed +PHP extensions and the PHP version in use. +In particular, [HHVM](http://hhvm.com/) may not offer the same filter functions +or parameters as Zend PHP. +Accessing an unknown filter function will result in a `RuntimeException`: + +```php +Filter\fun('unknown'); // throws RuntimeException +``` + +Some filters may accept or require additional filter parameters. +The optional `$parameters` argument will be passed to the filter handler as-is. +Please refer to the individual filter definition for more details. +For example, the `string.strip_tags` filter can be invoked like this: + +```php +$fun = Filter\fun('string.strip_tags', ''); + +$ret = $fun('h
i
'); +assert('hi' === $ret); +``` + +Under the hood, this function allocates a temporary memory stream, so it's +recommended to clean up the filter function after use. +Also, some filter functions (in particular the +[zlib compression filters](http://php.net/manual/en/filters.compression.php)) +may use internal buffers and may emit a final data chunk on close. +The filter function can be closed by invoking without any arguments: + +```php +$fun = Filter\fun('zlib.deflate'); + +$ret = $fun('hello') . $fun('world') . $fun(); +assert('helloworld' === gzinflate($ret)); +``` + +The filter function must not be used anymore after it has been closed. +Doing so will result in a `RuntimeException`: + +```php +$fun = Filter\fun('string.rot13'); +$fun(); + +$fun('test'); // throws RuntimeException +``` + +> Note: If you're using the zlib compression filters, then you should be wary +about engine inconsistencies between different PHP versions and HHVM. +These inconsistencies exist in the underlying PHP engines and there's little we +can do about this in this library. +Our test suite contains several test cases that exhibit these issues. +If you feel some test case is missing or outdated, we're happy to accept PRs! :) + ### remove() The `remove($filter)` function can be used to From 333dd979667db99195ddc59c4e565ceee931652b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 9 Nov 2015 00:33:12 +0100 Subject: [PATCH 7/7] Additional test cases for zlib compression filters --- README.md | 2 +- tests/FunZlibTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e37bc71..96ddb8e 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ $fun('test'); // throws RuntimeException about engine inconsistencies between different PHP versions and HHVM. These inconsistencies exist in the underlying PHP engines and there's little we can do about this in this library. -Our test suite contains several test cases that exhibit these issues. +[Our test suite](tests/) contains several test cases that exhibit these issues. If you feel some test case is missing or outdated, we're happy to accept PRs! :) ### remove() diff --git a/tests/FunZlibTest.php b/tests/FunZlibTest.php index 1ca2726..752c8a2 100644 --- a/tests/FunZlibTest.php +++ b/tests/FunZlibTest.php @@ -4,6 +4,15 @@ class BuiltInZlibTest extends PHPUnit_Framework_TestCase { + public function testFunZlibDeflateHelloWorld() + { + $deflate = StreamFilter\fun('zlib.deflate'); + + $data = $deflate('hello') . $deflate(' ') . $deflate('world') . $deflate(); + + $this->assertEquals(gzdeflate('hello world'), $data); + } + public function testFunZlibDeflateEmpty() { if (PHP_VERSION >= 7) $this->markTestSkipped('Not supported on PHP7 (empty string does not invoke filter)'); @@ -32,6 +41,24 @@ public function testFunZlibDeflateBig() $this->assertEquals($expected, gzinflate($bytes)); } + public function testFunZlibInflateHelloWorld() + { + $inflate = StreamFilter\fun('zlib.inflate'); + + $data = $inflate(gzdeflate('hello world')) . $inflate(); + + $this->assertEquals('hello world', $data); + } + + public function testFunZlibInflateEmpty() + { + $inflate = StreamFilter\fun('zlib.inflate'); + + $data = $inflate("\x03\x00") . $inflate(); + + $this->assertEquals('', $data); + } + public function testFunZlibInflateBig() { if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (final chunk will not be emitted)');