From 73f9daba74da303b9b462a85ae0ec963837e9e32 Mon Sep 17 00:00:00 2001 From: Zaahid Bateson Date: Mon, 16 Jul 2018 20:38:52 -0500 Subject: [PATCH] Refactor UUStream - Rename from UUStream, following GuzzleHttp's convention for its decorators - Don't bother implementing seek, user's can wrap in a CachingStream - Don't bother with getSize --- README.md | 2 +- src/{UUStreamDecorator.php => UUStream.php} | 228 +++++++++++------- ...reamDecoratorTest.php => UUStreamTest.php} | 136 ++++------- 3 files changed, 189 insertions(+), 177 deletions(-) rename src/{UUStreamDecorator.php => UUStream.php} (59%) rename tests/StreamDecorators/{UUStreamDecoratorTest.php => UUStreamTest.php} (66%) diff --git a/README.md b/README.md index 26aad32..b3aead7 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ while (($line = GuzzleHttp\Psr7\readline()) !== false) { The library consists of the following Psr\Http\Message\StreamInterface implementations: * ZBateson\StreamDecorators\QuotedPrintableStream * ZBateson\StreamDecorators\Base64Stream -* ZBateson\StreamDecorators\UUStreamDecorator +* ZBateson\StreamDecorators\UUStream * ZBateson\StreamDecorators\CharsetStream * ZBateson\StreamDecorators\NonClosingStream * ZBateson\StreamDecorators\ChunkSplitStream diff --git a/src/UUStreamDecorator.php b/src/UUStream.php similarity index 59% rename from src/UUStreamDecorator.php rename to src/UUStream.php index 96782bc..7e0a861 100644 --- a/src/UUStreamDecorator.php +++ b/src/UUStream.php @@ -7,35 +7,32 @@ namespace ZBateson\StreamDecorators; use Psr\Http\Message\StreamInterface; +use GuzzleHttp\Psr7\StreamDecoratorTrait; +use GuzzleHttp\Psr7\BufferStream; +use RuntimeException; /** * GuzzleHttp\Psr7 stream decoder extension for UU-Encoded streams. * - * Extends AbstractMimeTransferStreamDecorator, which prevents getSize and - * seeking to anywhere except the beginning (rewinding). - * * The size of the underlying stream and the position of bytes can't be * determined because the number of encoded bytes is indeterminate without * reading the entire stream. * * @author Zaahid Bateson */ -class UUStreamDecorator extends AbstractMimeTransferStreamDecorator +class UUStream implements StreamInterface { + use StreamDecoratorTrait; + /** * @var string name of the UUEncoded file */ protected $filename = null; /** - * @var string string of buffered bytes - */ - private $buffer = ''; - - /** - * @var int number of bytes in $buffer + * @var BufferStream of read and decoded bytes */ - private $bufferLength = 0; + private $buffer; /** * @var string remainder of write operation if the bytes didn't align to 3 @@ -44,14 +41,14 @@ class UUStreamDecorator extends AbstractMimeTransferStreamDecorator private $remainder = ''; /** - * @var boolean set to true when the UU header is written + * @var int read/write position */ - private $headerWritten = false; + private $position = 0; /** - * @var boolean set to true when the UU footer is written + * @var boolean set to true when 'write' is called */ - private $footerWritten = false; + private $isWriting = false; /** * @param StreamInterface $stream Stream to decorate @@ -59,17 +56,51 @@ class UUStreamDecorator extends AbstractMimeTransferStreamDecorator */ public function __construct(StreamInterface $stream, $filename = null) { - parent::__construct($stream); + $this->stream = $stream; $this->filename = $filename; + $this->buffer = new BufferStream(); + } + + /** + * Overridden to return the position in the target encoding. + * + * @return int + */ + public function tell() + { + return $this->position; + } + + /** + * Returns null, getSize isn't supported + * + * @return null + */ + public function getSize() + { + return null; + } + + /** + * Not supported. + * + * @param int $offset + * @param int $whence + * @throws RuntimeException + */ + public function seek($offset, $whence = SEEK_SET) + { + throw new RuntimeException('Cannot seek a UUStream'); } /** - * Resets the internal buffers. + * Overridden to return false + * + * @return boolean */ - protected function beforeSeek() { - $this->bufferLength = 0; - $this->buffer = ''; - $this->flush(); + public function isSeekable() + { + return false; } /** @@ -80,12 +111,12 @@ protected function beforeSeek() { */ private function readToEndOfLine($length) { - $str = $this->readRaw($length); + $str = $this->stream->read($length); if ($str === false || $str === '') { - return ''; + return $str; } while (substr($str, -1) !== "\n") { - $chr = $this->readRaw(1); + $chr = $this->stream->read(1); if ($chr === false || $chr === '') { break; } @@ -101,7 +132,7 @@ private function readToEndOfLine($length) * @param string $str * @return string */ - private function filterEncodedString($str) + private function filterAndDecode($str) { $ret = str_replace("\r", '', $str); $ret = preg_replace('/[^\x21-\xf5`\n]/', '`', $ret); @@ -110,65 +141,39 @@ private function filterEncodedString($str) if (preg_match('/^\s*begin\s+[^\s+]\s+([^\r\n]+)\s*$/im', $ret, $matches)) { $this->filename = $matches[1]; } - $ret = preg_replace('/^\s*begin[^\r\n]+\s*$|^\s*end\s*$/im', '', $ret); + $ret = preg_replace('/^\s*begin[^\r\n]+\s*$/im', '', $ret); } else { $ret = preg_replace('/^\s*end\s*$/im', '', $ret); } - return trim($ret); + return convert_uudecode(trim($ret)); } /** * Buffers bytes into $this->buffer, removing uuencoding headers and footers * and decoding them. */ - private function readRawBytesIntoBuffer() + private function fillBuffer($length) { // 5040 = 63 * 80, seems to be good balance for buffering in benchmarks // testing with a simple 'if ($length < x)' and calculating a better // size reduces speeds by up to 4x - $encoded = $this->filterEncodedString($this->readToEndOfLine(5040)); - if ($encoded === '') { - $this->buffer = ''; - } else { - $this->buffer = convert_uudecode($encoded); - } - $this->bufferLength = strlen($this->buffer); - } - - /** - * Attempts to fill up to $length bytes of decoded bytes into $this->buffer, - * and returns them. - * - * @param int $length - * @return string - */ - private function getDecodedBytes($length) - { - $data = $this->buffer; - $retLen = $this->bufferLength; - while ($retLen < $length) { - $this->readRawBytesIntoBuffer($length); - if ($this->bufferLength === 0) { + while ($this->buffer->getSize() < $length) { + $read = $this->readToEndOfLine(5040); + if ($read === false || $read === '') { break; } - $retLen += $this->bufferLength; - $data .= $this->buffer; + $this->buffer->write($this->filterAndDecode($read)); } - $ret = substr($data, 0, $length); - $this->buffer = substr($data, $length); - $this->bufferLength = strlen($this->buffer); - $this->position += strlen($ret); - return $ret; } /** * Returns true if the end of stream has been reached. * - * @return type + * @return boolean */ public function eof() { - return ($this->bufferLength === 0 && parent::eof()); + return ($this->buffer->eof() && $this->stream->eof()); } /** @@ -181,9 +186,12 @@ public function read($length) { // let Guzzle decide what to do. if ($length <= 0 || $this->eof()) { - return $this->readRaw($length); + return $this->stream->read($length); } - return $this->getDecodedBytes($length); + $this->fillBuffer($length); + $read = $this->buffer->read($length); + $this->position += strlen($read); + return $read; } /** @@ -192,8 +200,7 @@ public function read($length) private function writeUUHeader() { $filename = (empty($this->filename)) ? 'null' : $this->filename; - $this->writeRaw("begin 666 $filename"); - $this->headerWritten = true; + $this->stream->write("begin 666 $filename"); } /** @@ -201,7 +208,7 @@ private function writeUUHeader() */ private function writeUUFooter() { - $this->writeRaw("\r\n`\r\nend\r\n"); + $this->stream->write("\r\n`\r\nend\r\n"); $this->footerWritten = true; } @@ -214,7 +221,27 @@ private function writeEncoded($bytes) { $encoded = preg_replace('/\r\n|\r|\n/', "\r\n", rtrim(convert_uuencode($bytes))); // removes ending '`' line - $this->writeRaw("\r\n" . rtrim(substr($encoded, 0, -1))); + $this->stream->write("\r\n" . rtrim(substr($encoded, 0, -1))); + } + + /** + * Prepends any existing remainder to the passed string, then checks if the + * string fits into a uuencoded line, and removes and keeps any remainder + * from the string to write. Full lines ready for writing are returned. + * + * @param string $string + * @return string + */ + private function handleRemainder($string) + { + $write = $this->remainder . $string; + $nRem = strlen($write) % 45; + $this->remainder = ''; + if ($nRem !== 0) { + $this->remainder = substr($write, -$nRem); + $write = substr($write, 0, -$nRem); + } + return $write; } /** @@ -223,27 +250,21 @@ private function writeEncoded($bytes) * Note that reading and writing to the same stream without rewinding is not * supported. * - * Also note that some bytes may not be written until close, detach, seek or - * flush are called. This happens if written data doesn't align to a - * complete uuencoded 'line' of 45 bytes. In addition, the UU footer is - * only written when one of the mentioned methods are called. + * Also note that some bytes may not be written until close or detach are + * called. This happens if written data doesn't align to a complete + * uuencoded 'line' of 45 bytes. In addition, the UU footer is only written + * when closing or detaching as well. * * @param string $string * @return int the number of bytes written */ public function write($string) { + $this->isWriting = true; if ($this->position === 0) { $this->writeUUHeader(); } - $write = $this->remainder . $string; - $nRem = strlen($write) % 45; - - $this->remainder = ''; - if ($nRem !== 0) { - $this->remainder = substr($write, -$nRem); - $write = substr($write, 0, -$nRem); - } + $write = $this->handleRemainder($string); if ($write !== '') { $this->writeEncoded($write); } @@ -252,21 +273,6 @@ public function write($string) return $written; } - /** - * Writes out any remaining bytes to the underlying stream. - */ - public function flush() - { - if ($this->remainder !== '') { - $this->writeEncoded($this->remainder); - } - $this->remainder = ''; - if ($this->headerWritten && !$this->footerWritten) { - $this->writeUUFooter(); - } - parent::flush(); - } - /** * Returns the filename set in the UUEncoded header (or null) * @@ -286,4 +292,40 @@ public function setFilename($filename) { $this->filename = $filename; } + + /** + * Writes out any remaining bytes and the UU footer. + */ + private function beforeClose() + { + if (!$this->isWriting) { + return; + } + if ($this->remainder !== '') { + $this->writeEncoded($this->remainder); + } + $this->remainder = ''; + $this->isWriting = false; + $this->writeUUFooter(); + } + + /** + * Writes any remaining bytes out followed by the uu-encoded footer, then + * closes the stream. + */ + public function close() + { + $this->beforeClose(); + $this->stream->close(); + } + + /** + * Writes any remaining bytes out followed by the uu-encoded footer, then + * detaches the stream. + */ + public function detach() + { + $this->beforeClose(); + $this->stream->detach(); + } } diff --git a/tests/StreamDecorators/UUStreamDecoratorTest.php b/tests/StreamDecorators/UUStreamTest.php similarity index 66% rename from tests/StreamDecorators/UUStreamDecoratorTest.php rename to tests/StreamDecorators/UUStreamTest.php index 2187faf..5b7055a 100644 --- a/tests/StreamDecorators/UUStreamDecoratorTest.php +++ b/tests/StreamDecorators/UUStreamTest.php @@ -6,14 +6,13 @@ use GuzzleHttp\Psr7\StreamWrapper; /** - * Description of UUStreamDecoratorTest + * Description of UUStreamTest * - * @group UUStreamDecorator - * @covers ZBateson\StreamDecorators\AbstractMimeTransferStreamDecorator - * @covers ZBateson\StreamDecorators\UUStreamDecorator + * @group UUStream + * @covers ZBateson\StreamDecorators\UUStream * @author Zaahid Bateson */ -class UUStreamDecoratorTest extends PHPUnit_Framework_TestCase +class UUStreamTest extends PHPUnit_Framework_TestCase { public function testReadAndRewind() { @@ -24,10 +23,9 @@ public function testReadAndRewind() . 'musique est vulgaire ils te fabriquent pour te la vendre une âme ' . 'vulgaire.é', 10); $stream = Psr7\stream_for(convert_uuencode($str)); - $uuStream = new UUStreamDecorator($stream); - for ($i = 1; $i < strlen($str); ++$i) { - $uuStream->rewind(); + $stream->rewind(); + $uuStream = new UUStream(new NonClosingStream($stream)); for ($j = 0; $j < strlen($str); $j += $i) { $this->assertEquals(substr($str, $j, $i), $uuStream->read($i), "Read $j failed at $i step"); } @@ -44,11 +42,11 @@ public function testReadWithCrLf() . 'musique est vulgaire ils te fabriquent pour te la vendre une âme ' . 'vulgaire.é', 10); $encoded = preg_replace('/([^\r]?)\n/', "$1\r\n", convert_uuencode($str)); - $stream = Psr7\stream_for($encoded); - $uuStream = new UUStreamDecorator($stream); + $stream = Psr7\stream_for($encoded); for ($i = 1; $i < strlen($str); ++$i) { - $uuStream->rewind(); + $stream->rewind(); + $uuStream = new UUStream(new NonClosingStream($stream)); for ($j = 0; $j < strlen($str); $j += $i) { $this->assertEquals(substr($str, $j, $i), $uuStream->read($i), "Read $j failed at $i step"); } @@ -67,7 +65,7 @@ public function testReadContents() for ($i = 0; $i < strlen($str); ++$i) { $substr = substr($str, 0, $i + 1); $stream = Psr7\stream_for(convert_uuencode($substr)); - $uuStream = new UUStreamDecorator($stream); + $uuStream = new UUStream($stream); $this->assertEquals($substr, $uuStream->getContents()); } } @@ -82,7 +80,7 @@ public function testReadToEof() . 'vulgaire.é'; for ($i = 0; $i < strlen($str); ++$i) { $stream = Psr7\stream_for(convert_uuencode(substr($str, $i))); - $uuStream = new UUStreamDecorator($stream); + $uuStream = new UUStream($stream); for ($j = $i; !$uuStream->eof(); ++$j) { $this->assertEquals(substr($str, $j, 1), $uuStream->read(1), "Failed reading to EOF on substr $i iteration $j"); } @@ -91,21 +89,12 @@ public function testReadToEof() public function testGetSize() { - $str = 'é J\'interdis aux marchands de vanter trop leur marchandises. Car ' - . 'ils se font vite pédagogues et t\'enseignent comme but ce qui ' - . 'n\'est par essence qu\'un moyen, et te trompant ainsi sur la ' - . 'route à suivre les voilà bientôt qui te dégradent, car si leur ' - . 'musique est vulgaire ils te fabriquent pour te la vendre une âme ' - . 'vulgaire.é'; - - $stream = Psr7\stream_for(convert_uuencode($str)); - $uuStream = new UUStreamDecorator($stream); - for ($i = 0; $i < strlen($str); ++$i) { - $this->assertEquals(strlen($str), $uuStream->getSize()); - $this->assertEquals(substr($str, $i, 1), $uuStream->read(1), "Failed reading to EOF on substr $i"); - } + $str = 'Sweetest little pie'; + $stream = Psr7\stream_for(quoted_printable_encode($str)); + $uuStream = new UUStream($stream); + $this->assertNull($uuStream->getSize()); } - + public function testTell() { $str = 'é J\'interdis aux marchands de vanter trop leur marchandises. Car ' @@ -114,11 +103,11 @@ public function testTell() . 'route à suivre les voilà bientôt qui te dégradent, car si leur ' . 'musique est vulgaire ils te fabriquent pour te la vendre une âme ' . 'vulgaire.é'; - $stream = Psr7\stream_for(convert_uuencode($str)); - $uuStream = new UUStreamDecorator($stream); + $stream = Psr7\stream_for(convert_uuencode($str)); for ($i = 1; $i < strlen($str); ++$i) { - $uuStream->rewind(); + $stream->rewind(); + $uuStream = new UUStream(new NonClosingStream($stream)); for ($j = 0; $j < strlen($str); $j += $i) { $this->assertEquals($j, $uuStream->tell(), "Tell at $j failed with $i step"); $uuStream->read($i); @@ -127,25 +116,13 @@ public function testTell() } } - public function testSeekCur() - { - $stream = Psr7\stream_for(convert_uuencode('test')); - $uuStream = new UUStreamDecorator($stream); - $this->assertEquals('te', $uuStream->read(2)); - $uuStream->seek(-2, SEEK_CUR); - $this->assertEquals('te', $uuStream->read(2)); - $uuStream->seek(1, SEEK_CUR); - $this->assertEquals('t', $uuStream->read(1)); - } - - public function testSeek() + public function testSeekUnsopported() { - $stream = Psr7\stream_for(convert_uuencode('0123456789')); - $uuStream = new UUStreamDecorator($stream); - $uuStream->seek(4); - $this->assertEquals('4', $uuStream->read(1)); - $uuStream->seek(-1, SEEK_END); - $this->assertEquals('9', $uuStream->read(1)); + $stream = Psr7\stream_for(quoted_printable_encode('Sweetest little pie')); + $test = new UUStream($stream); + $this->assertFalse($test->isSeekable()); + $this->setExpectedException('RuntimeException'); + $test->seek(0); } public function testReadWithBeginAndEnd() @@ -164,7 +141,7 @@ public function testReadWithBeginAndEnd() $encoded = "begin 666 devil.txt\r\n\r\n" . $encoded . "\r\nend\r\n"; $stream = Psr7\stream_for($encoded); - $uuStream = new UUStreamDecorator($stream); + $uuStream = new UUStream($stream); $this->assertEquals($substr, $uuStream->getContents()); } } @@ -173,30 +150,16 @@ public function testDecodeFile() { $encoded = './tests/_data/blueball.uu.txt'; $org = './tests/_data/blueball.png'; - $f = fopen($encoded, 'r'); - - $streamDecorator = new UUStreamDecorator(Psr7\stream_for($f)); - $handle = StreamWrapper::getResource($streamDecorator); - - $this->assertEquals(file_get_contents($org), stream_get_contents($handle), 'Decoded blueball not equal to original file'); - - fclose($handle); - fclose($f); + $stream = new UUStream(Psr7\stream_for(fopen($encoded, 'r'))); + $this->assertEquals(file_get_contents($org), $stream->getContents(), 'Decoded blueball not equal to original file'); } public function testDecodeFileWithSpaces() { $encoded = './tests/_data/blueball-2.uu.txt'; $org = './tests/_data/blueball.png'; - $f = fopen($encoded, 'r'); - - $streamDecorator = new UUStreamDecorator(Psr7\stream_for($f)); - $handle = StreamWrapper::getResource($streamDecorator); - - $this->assertEquals(file_get_contents($org), stream_get_contents($handle), 'Decoded blueball not equal to original file'); - - fclose($handle); - fclose($f); + $stream = new UUStream(Psr7\stream_for(fopen($encoded, 'r'))); + $this->assertEquals(file_get_contents($org), $stream->getContents(), 'Decoded blueball not equal to original file'); } public function testWrite() @@ -205,21 +168,25 @@ public function testWrite() $contents = file_get_contents($org); for ($i = 1; $i < strlen($contents); ++$i) { - $f = fopen('php://temp', 'r+'); - $streamDecorator = new UUStreamDecorator(Psr7\stream_for($f)); + $stream = Psr7\stream_for(fopen('php://temp', 'r+')); + $out = new UUStream(new NonClosingStream($stream)); for ($j = 0; $j < strlen($contents); $j += $i) { - $streamDecorator->write(substr($contents, $j, $i)); + $out->write(substr($contents, $j, $i)); } - $streamDecorator->rewind(); - $this->assertEquals($contents, $streamDecorator->getContents()); - rewind($f); - $raw = stream_get_contents($f); + $out->close(); + + $stream->rewind(); + $in = new UUStream(new NonClosingStream($stream)); + $this->assertEquals($contents, $in->getContents()); + $in->close(); + + $stream->rewind(); + $raw = $stream->getContents(); $arr = explode("\r\n", $raw); $this->assertGreaterThan(0, count($arr)); for ($x = 0; $x < count($arr); ++$x) { $this->assertLessThanOrEqual(61, strlen($arr[$x])); } - fclose($f); } } @@ -234,21 +201,24 @@ public function testWriteDifferentContentLengths() for ($i = 1; $i < strlen($contents); ++$i) { $str = substr($contents, 0, strlen($contents) - $i); - $f = fopen('php://temp', 'r+'); - $streamDecorator = new UUStreamDecorator(Psr7\stream_for($f)); + $stream = Psr7\stream_for(fopen('php://temp', 'r+')); + $out = new UUStream(new NonClosingStream($stream)); for ($j = 0; $j < strlen($str); $j += $i) { - $streamDecorator->write(substr($str, $j, $i)); + $out->write(substr($str, $j, $i)); } - $streamDecorator->rewind(); - $this->assertEquals($str, $streamDecorator->getContents()); - rewind($f); - $raw = stream_get_contents($f); + $out->close(); + + $stream->rewind(); + $in = new UUStream(new NonClosingStream($stream)); + $this->assertEquals($str, $in->getContents()); + $stream->rewind(); + + $raw = $stream->getContents(); $arr = explode("\r\n", $raw); $this->assertGreaterThan(0, count($arr)); for ($x = 0; $x < count($arr); ++$x) { $this->assertLessThanOrEqual(61, strlen($arr[$x])); } - fclose($f); } } }