Skip to content

Commit

Permalink
Refactor UUStream
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
zbateson committed Jul 17, 2018
1 parent 24daad9 commit 73f9dab
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 177 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
228 changes: 135 additions & 93 deletions src/UUStreamDecorator.php → src/UUStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,32 +41,66 @@ 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
* @param string optional file name
*/
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;
}

/**
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -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());
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -192,16 +200,15 @@ 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");
}

/**
* Writes the '`' and 'end' UU footer lines.
*/
private function writeUUFooter()
{
$this->writeRaw("\r\n`\r\nend\r\n");
$this->stream->write("\r\n`\r\nend\r\n");
$this->footerWritten = true;
}

Expand All @@ -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;
}

/**
Expand All @@ -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);
}
Expand All @@ -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)
*
Expand All @@ -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();
}
}
Loading

0 comments on commit 73f9dab

Please sign in to comment.