glastree_on_gitea

This commit is contained in:
2026-05-26 08:14:29 +02:00
commit 0bed099d05
9556 changed files with 1186307 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
<?php
/**
* This file is part of the ZBateson\StreamDecorators project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\StreamDecorators;
use GuzzleHttp\Psr7\BufferStream;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
/**
* GuzzleHttp\Psr7 stream decoder extension for base64 streams.
*
* Note that it's expected the underlying stream will only contain valid base64
* characters (normally the stream should be wrapped in a
* PregReplaceFilterStream to filter out non-base64 characters for reading).
*
* ```
* $f = fopen(...);
* $stream = new Base64Stream(new PregReplaceFilterStream(
* Psr7\Utils::streamFor($f), '/[^a-zA-Z0-9\/\+=]/', ''
* ));
* //...
* ```
*
* For writing, a ChunkSplitStream could come in handy so the output is split
* into lines:
*
* ```
* $f = fopen(...);
* $stream = new Base64Stream(new ChunkSplitStream(new PregReplaceFilterStream(
* Psr7\Utils::streamFor($f), '/[^a-zA-Z0-9\/\+=]/', ''
* )));
* //...
* ```
*
* @author Zaahid Bateson
*/
class Base64Stream implements StreamInterface
{
use StreamDecoratorTrait;
/**
* @var BufferStream buffered bytes
*/
private BufferStream $buffer;
/**
* @var string remainder of write operation if the bytes didn't align to 3
* bytes
*/
private string $remainder = '';
/**
* @var int current number of read/written bytes (for tell())
*/
private int $position = 0;
/**
* @var StreamInterface $stream
*/
private StreamInterface $stream;
public function __construct(StreamInterface $stream)
{
$this->stream = $stream;
$this->buffer = new BufferStream();
}
/**
* Returns the current position of the file read/write pointer
*/
public function tell() : int
{
return $this->position;
}
/**
* Returns null, getSize isn't supported
*
* @return null
*/
public function getSize() : ?int
{
return null;
}
/**
* Not implemented (yet).
*
* Seek position can be calculated.
*
* @param int $offset
* @param int $whence
* @throws RuntimeException
*/
public function seek($offset, $whence = SEEK_SET) : void
{
throw new RuntimeException('Cannot seek a Base64Stream');
}
/**
* Overridden to return false
*/
public function isSeekable() : bool
{
return false;
}
/**
* Returns true if the end of stream has been reached.
*/
public function eof() : bool
{
return ($this->buffer->eof() && $this->stream->eof());
}
/**
* Fills the internal byte buffer after reading and decoding data from the
* underlying stream.
*
* Note that it's expected the underlying stream will only contain valid
* base64 characters (normally the stream should be wrapped in a
* PregReplaceFilterStream to filter out non-base64 characters).
*/
private function fillBuffer(int $length) : void
{
$fill = 8192;
while ($this->buffer->getSize() < $length) {
$read = $this->stream->read($fill);
if ($read === '') {
break;
}
$this->buffer->write(\base64_decode($read));
}
}
/**
* Attempts to read $length bytes after decoding them, and returns them.
*
* Note that reading and writing to the same stream may result in wrongly
* encoded data and is not supported.
*
* @param int $length
*/
public function read($length) : string
{
// let Guzzle decide what to do.
if ($length <= 0 || $this->eof()) {
return $this->stream->read($length);
}
$this->fillBuffer($length);
$ret = $this->buffer->read($length);
$this->position += \strlen($ret);
return $ret;
}
/**
* Writes the passed string to the underlying stream after encoding it to
* base64.
*
* Base64Stream::close or detach must be called. Failing to do so may
* result in 1-2 bytes missing from the end of the stream if there's a
* remainder. Note that the default Stream destructor calls close as well.
*
* Note that reading and writing to the same stream may result in wrongly
* encoded data and is not supported.
*
* @param string $string
* @return int the number of bytes written
*/
public function write($string) : int
{
$bytes = $this->remainder . $string;
$len = \strlen($bytes);
if (($len % 3) !== 0) {
$this->remainder = \substr($bytes, -($len % 3));
$bytes = \substr($bytes, 0, $len - ($len % 3));
} else {
$this->remainder = '';
}
$this->stream->write(\base64_encode($bytes));
$written = \strlen($string);
$this->position += $len;
return $written;
}
/**
* Writes out any remaining bytes at the end of the stream and closes.
*/
private function beforeClose() : void
{
if ($this->isWritable() && $this->remainder !== '') {
$this->stream->write(\base64_encode($this->remainder));
$this->remainder = '';
}
}
/**
* @inheritDoc
*/
public function close() : void
{
$this->beforeClose();
$this->stream->close();
}
/**
* @inheritDoc
*/
public function detach()
{
$this->beforeClose();
$this->stream->detach();
return null;
}
}
+179
View File
@@ -0,0 +1,179 @@
<?php
/**
* This file is part of the ZBateson\StreamDecorator project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\StreamDecorators;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
use ZBateson\MbWrapper\MbWrapper;
/**
* GuzzleHttp\Psr7 stream decoder extension for charset conversion.
*
* @author Zaahid Bateson
*/
class CharsetStream implements StreamInterface
{
use StreamDecoratorTrait;
/**
* @var MbWrapper the charset converter
*/
protected MbWrapper $converter;
/**
* @var string charset of the source stream
*/
protected string $streamCharset = 'ISO-8859-1';
/**
* @var string charset of strings passed in write operations, and returned
* in read operations.
*/
protected string $stringCharset = 'UTF-8';
/**
* @var int current read/write position
*/
private int $position = 0;
/**
* @var int number of $stringCharset characters in $buffer
*/
private int $bufferLength = 0;
/**
* @var string a buffer of characters read in the original $streamCharset
* encoding
*/
private string $buffer = '';
/**
* @var StreamInterface $stream
*/
private StreamInterface $stream;
/**
* @param StreamInterface $stream Stream to decorate
* @param string $streamCharset The underlying stream's charset
* @param string $stringCharset The charset to encode strings to (or
* expected for write)
*/
public function __construct(StreamInterface $stream, string $streamCharset = 'ISO-8859-1', string $stringCharset = 'UTF-8')
{
$this->stream = $stream;
$this->converter = new MbWrapper();
$this->streamCharset = $streamCharset;
$this->stringCharset = $stringCharset;
}
/**
* Overridden to return the position in the target encoding.
*/
public function tell() : int
{
return $this->position;
}
/**
* Returns null, getSize isn't supported
*
* @return null
*/
public function getSize() : ?int
{
return null;
}
/**
* Not supported.
*
* @param int $offset
* @param int $whence
* @throws RuntimeException
*/
public function seek($offset, $whence = SEEK_SET) : void
{
throw new RuntimeException('Cannot seek a CharsetStream');
}
/**
* Overridden to return false
*/
public function isSeekable() : bool
{
return false;
}
/**
* Reads a minimum of $length characters from the underlying stream in its
* encoding into $this->buffer.
*
* Aligning to 4 bytes seemed to solve an issue reading from UTF-16LE
* streams and pass testReadUtf16LeToEof, although the buffered string
* should've solved that on its own.
*/
private function readRawCharsIntoBuffer(int $length) : void
{
$n = (int) \ceil(($length + 32) / 4.0) * 4;
while ($this->bufferLength < $n) {
$raw = $this->stream->read($n + 512);
if ($raw === '') {
return;
}
$this->buffer .= $raw;
$this->bufferLength = $this->converter->getLength($this->buffer, $this->streamCharset);
}
}
/**
* Returns true if the end of stream has been reached.
*/
public function eof() : bool
{
return ($this->bufferLength === 0 && $this->stream->eof());
}
/**
* Reads up to $length decoded chars from the underlying stream and returns
* them after converting to the target string charset.
*
* @param int $length
*/
public function read($length) : string
{
// let Guzzle decide what to do.
if ($length <= 0 || $this->eof()) {
return $this->stream->read($length);
}
$this->readRawCharsIntoBuffer($length);
$numChars = \min([$this->bufferLength, $length]);
$chars = $this->converter->getSubstr($this->buffer, $this->streamCharset, 0, $numChars);
$this->position += $numChars;
$this->buffer = $this->converter->getSubstr($this->buffer, $this->streamCharset, $numChars);
$this->bufferLength -= $numChars;
return $this->converter->convert($chars, $this->streamCharset, $this->stringCharset);
}
/**
* Writes the passed string to the underlying stream after converting it to
* the target stream encoding.
*
* @param string $string
* @return int the number of bytes written
*/
public function write($string) : int
{
$converted = $this->converter->convert($string, $this->stringCharset, $this->streamCharset);
$written = $this->converter->getLength($converted, $this->streamCharset);
$this->position += $written;
return $this->stream->write($converted);
}
}
@@ -0,0 +1,122 @@
<?php
/**
* This file is part of the ZBateson\StreamDecorators project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\StreamDecorators;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
/**
* Inserts line ending characters after the set number of characters have been
* written to the underlying stream.
*
* @author Zaahid Bateson
*/
class ChunkSplitStream implements StreamInterface
{
use StreamDecoratorTrait;
/**
* @var int Number of bytes written, and importantly, if non-zero, writes a
* final $lineEnding on close (and so maintained instead of using
* tell() directly)
*/
private int $position;
/**
* @var int The number of characters in a line before inserting $lineEnding.
*/
private int $lineLength;
/**
* @var string The line ending characters to insert.
*/
private string $lineEnding;
/**
* @var int The strlen() of $lineEnding
*/
private int $lineEndingLength;
/**
* @var StreamInterface $stream
*/
private StreamInterface $stream;
public function __construct(StreamInterface $stream, int $lineLength = 76, string $lineEnding = "\r\n")
{
$this->stream = $stream;
$this->position = 0;
$this->lineLength = $lineLength;
$this->lineEnding = $lineEnding;
$this->lineEndingLength = \strlen($this->lineEnding);
}
/**
* Inserts the line ending character after each line length characters in
* the passed string, making sure previously written bytes are taken into
* account.
*/
private function getChunkedString(string $string) : string
{
$firstLine = '';
if ($this->tell() !== 0) {
$next = $this->lineLength - ($this->position % ($this->lineLength + $this->lineEndingLength));
if (\strlen($string) > $next) {
$firstLine = \substr($string, 0, $next) . $this->lineEnding;
$string = \substr($string, $next);
}
}
// chunk_split always ends with the passed line ending
$chunked = $firstLine . \chunk_split($string, $this->lineLength, $this->lineEnding);
return \substr($chunked, 0, \strlen($chunked) - $this->lineEndingLength);
}
/**
* Writes the passed string to the underlying stream, ensuring line endings
* are inserted every "line length" characters in the string.
*
* @param string $string
* @return int number of bytes written
*/
public function write($string) : int
{
$chunked = $this->getChunkedString($string);
$this->position += \strlen($chunked);
return $this->stream->write($chunked);
}
/**
* Inserts a final line ending character.
*/
private function beforeClose() : void
{
if ($this->position !== 0) {
$this->stream->write($this->lineEnding);
}
}
/**
* @inheritDoc
*/
public function close() : void
{
$this->beforeClose();
$this->stream->close();
}
/**
* @inheritDoc
*/
public function detach()
{
$this->beforeClose();
$this->stream->detach();
return null;
}
}
@@ -0,0 +1,175 @@
<?php
/**
* This file is part of the ZBateson\StreamDecorators project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\StreamDecorators;
use Psr\Http\Message\StreamInterface;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use GuzzleHttp\Psr7\BufferStream;
use GuzzleHttp\Psr7\CachingStream;
use GuzzleHttp\Psr7\FnStream;
use GuzzleHttp\Psr7\Utils;
/**
* A version of Guzzle's CachingStream that will read bytes from one stream,
* write them into another decorated stream, and read them back from a 3rd,
* undecorated, buffered stream where the bytes are written to.
*
* A read operation is basically:
*
* Read from A, write to B (which decorates C), read and return from C (which is
* backed by a BufferedStream).
*
* Note that the DecoratedCachingStream doesn't support write operations.
*/
class DecoratedCachingStream implements StreamInterface
{
use StreamDecoratorTrait;
/**
* @var StreamInterface the stream to read from and fill writeStream with
*/
private StreamInterface $readStream;
/**
* @var StreamInterface the underlying undecorated stream to read from,
* where $writeStream is being written to
*/
private StreamInterface $stream;
/**
* @var StreamInterface decorated $stream that will be written to for
* caching that wraps $stream. Once filled, the stream is closed so it
* supports a Base64Stream which writes bytes at the end.
*/
private ?StreamInterface $writeStream;
/**
* @var int Minimum buffer read length. At least this many bytes will be
* read and cached into $writeStream on each call to read from
* $readStream
*/
private int $minBytesCache;
/**
* @param StreamInterface $stream Stream to cache. The cursor is assumed to
* be at the beginning of the stream.
* @param callable(StreamInterface) : StreamInterface $decorator takes the
* passed StreamInterface and decorates it, and returns the decorated
* StreamInterface
*/
public function __construct(
StreamInterface $stream,
callable $decorator,
int $minBytesCache = 16384
) {
$this->readStream = $stream;
$bufferStream = new TellZeroStream(new BufferStream());
$this->stream = new CachingStream($bufferStream);
$this->writeStream = $decorator(new NonClosingStream($bufferStream));
$this->minBytesCache = $minBytesCache;
}
public function getSize(): ?int
{
// the decorated stream could be a different size
$this->cacheEntireStream();
return $this->stream->getSize();
}
public function rewind(): void
{
$this->seek(0);
}
public function seek($offset, $whence = SEEK_SET): void
{
if ($whence === SEEK_SET) {
$byte = $offset;
} elseif ($whence === SEEK_CUR) {
$byte = $offset + $this->tell();
} elseif ($whence === SEEK_END) {
$size = $this->getSize();
$byte = $size + $offset;
} else {
throw new \InvalidArgumentException('Invalid whence');
}
$diff = $byte - $this->stream->getSize();
if ($diff > 0) {
// Read the remoteStream until we have read in at least the amount
// of bytes requested, or we reach the end of the file.
while ($diff > 0 && !$this->readStream->eof()) {
$this->read($diff);
$diff = $byte - $this->stream->getSize();
}
} else {
// We can just do a normal seek since we've already seen this byte.
$this->stream->seek($byte);
}
}
private function cacheBytes(int $size) : void {
if (!$this->readStream->eof()) {
$data = $this->readStream->read(max($this->minBytesCache, $size));
$this->writeStream->write($data);
if ($this->readStream->eof()) {
// needed because Base64Stream writes bytes on closing
$this->writeStream->close();
$this->writeStream = null;
}
}
}
public function read($length): string
{
$data = $this->stream->read($length);
$remaining = $length - strlen($data);
if ($remaining > 0) {
$this->cacheBytes($remaining);
$data .= $this->stream->read($remaining);
}
return $data;
}
public function isWritable(): bool
{
return false;
}
public function write($string): int
{
throw new \RuntimeException('Cannot write to a DecoratedCachingStream');
}
public function eof(): bool
{
return $this->stream->eof() && $this->readStream->eof();
}
/**
* Close both the remote stream and buffer stream
*/
public function close(): void
{
$this->readStream->close();
$this->stream->close();
if ($this->writeStream !== null) {
$this->writeStream->close();
}
}
private function cacheEntireStream(): int
{
// as-is from CachingStream
$target = new FnStream(['write' => 'strlen']);
Utils::copyToStream($this, $target);
return $this->tell();
}
}
@@ -0,0 +1,67 @@
<?php
/**
* This file is part of the ZBateson\StreamDecorators project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\StreamDecorators;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
/**
* Doesn't close the underlying stream when 'close' is called on it. Instead,
* calling close simply removes any reference to the underlying stream. Note
* that GuzzleHttp\Psr7\Stream calls close in __destruct, so a reference to the
* Stream needs to be kept. For example:
*
* ```
* $f = fopen('php://temp', 'r+');
* $test = new NonClosingStream(Psr7\Utils::streamFor('test'));
* // work
* $test->close();
* rewind($f); // error, $f is a closed resource
* ```
*
* Instead, this would work:
*
* ```
* $stream = Psr7\Utils::streamFor(fopen('php://temp', 'r+'));
* $test = new NonClosingStream($stream);
* // work
* $test->close();
* $stream->rewind(); // works
* ```
*
* @author Zaahid Bateson
*/
class NonClosingStream implements StreamInterface
{
use StreamDecoratorTrait;
/**
* @var ?StreamInterface $stream
* @phpstan-ignore-next-line
*/
private ?StreamInterface $stream;
/**
* @inheritDoc
*/
public function close() : void
{
$this->stream = null; // @phpstan-ignore-line
}
/**
* Overridden to detach the underlying stream without closing it.
*
* @inheritDoc
*/
public function detach()
{
$this->stream = null; // @phpstan-ignore-line
return null;
}
}
@@ -0,0 +1,110 @@
<?php
/**
* This file is part of the ZBateson\StreamDecorators project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\StreamDecorators;
use GuzzleHttp\Psr7\BufferStream;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
/**
* Calls preg_replace on each read operation with the passed pattern and
* replacement string. Should only really be used to find single characters,
* since a pattern intended to match more may be split across multiple read()
* operations.
*
* @author Zaahid Bateson
*/
class PregReplaceFilterStream implements StreamInterface
{
use StreamDecoratorTrait;
/**
* @var string The regex pattern
*/
private string $pattern;
/**
* @var string The replacement
*/
private string $replacement;
/**
* @var BufferStream Buffered stream of input from the underlying stream
*/
private BufferStream $buffer;
/**
* @var StreamInterface $stream
*/
private StreamInterface $stream;
public function __construct(StreamInterface $stream, string $pattern, string $replacement)
{
$this->stream = $stream;
$this->pattern = $pattern;
$this->replacement = $replacement;
$this->buffer = new BufferStream();
}
/**
* Returns true if the end of stream has been reached.
*/
public function eof() : bool
{
return ($this->buffer->eof() && $this->stream->eof());
}
/**
* Not supported by PregReplaceFilterStream
*
* @param int $offset
* @param int $whence
* @throws RuntimeException
*/
public function seek($offset, $whence = SEEK_SET) : void
{
throw new RuntimeException('Cannot seek a PregReplaceFilterStream');
}
/**
* Overridden to return false
*/
public function isSeekable() : bool
{
return false;
}
/**
* Fills the BufferStream with at least 8192 characters of input for future
* read operations.
*/
private function fillBuffer(int $length) : void
{
$fill = (int) \max([$length, 8192]);
while ($this->buffer->getSize() < $length) {
$read = $this->stream->read($fill);
if ($read === '') {
break;
}
$this->buffer->write(\preg_replace($this->pattern, $this->replacement, $read));
}
}
/**
* Reads from the underlying stream, filters it and returns up to $length
* bytes.
*
* @param int $length
*/
public function read($length) : string
{
$this->fillBuffer($length);
return $this->buffer->read($length);
}
}
@@ -0,0 +1,222 @@
<?php
/**
* This file is part of the ZBateson\StreamDecorators project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\StreamDecorators;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
/**
* GuzzleHttp\Psr7 stream decoder decorator for quoted printable streams.
*
* @author Zaahid Bateson
*/
class QuotedPrintableStream implements StreamInterface
{
use StreamDecoratorTrait;
/**
* @var int current read/write position
*/
private int $position = 0;
/**
* @var string Last line of written text (used to maintain good line-breaks)
*/
private string $lastLine = '';
/**
* @var StreamInterface $stream
* @phpstan-ignore-next-line
*/
private StreamInterface $stream;
/**
* Overridden to return the position in the target encoding.
*/
public function tell() : int
{
return $this->position;
}
/**
* Returns null, getSize isn't supported
*
* @return null
*/
public function getSize() : ?int
{
return null;
}
/**
* Not supported.
*
* @param int $offset
* @param int $whence
* @throws RuntimeException
*/
public function seek($offset, $whence = SEEK_SET) : void
{
throw new RuntimeException('Cannot seek a QuotedPrintableStream');
}
/**
* Overridden to return false
*/
public function isSeekable() : bool
{
return false;
}
/**
* Reads $length chars from the underlying stream, prepending the past $pre
* to it first.
*
* If the characters read (including the prepended $pre) contain invalid
* quoted-printable characters, the underlying stream is rewound by the
* total number of characters ($length + strlen($pre)).
*
* The quoted-printable encoded characters are returned. If the characters
* read are invalid, '3D' is returned indicating an '=' character.
*/
private function readEncodedChars(int $length, string $pre = '') : string
{
$str = $pre . $this->stream->read($length);
$len = \strlen($str);
if ($len > 0 && !\preg_match('/^[0-9a-f]{2}$|^[\r\n]{1,2}.?$/is', $str) && $this->stream->isSeekable()) {
$this->stream->seek(-$len, SEEK_CUR);
return '3D'; // '=' character
}
return $str;
}
/**
* Decodes the passed $block of text.
*
* If the last or before last character is an '=' char, indicating the
* beginning of a quoted-printable encoded char, 1 or 2 additional bytes are
* read from the underlying stream respectively.
*
* @return string The decoded string
*/
private function decodeBlock(string $block) : string
{
if (\substr($block, -1) === '=') {
$block .= $this->readEncodedChars(2);
} elseif (\substr($block, -2, 1) === '=') {
$first = \substr($block, -1);
$block = \substr($block, 0, -1);
$block .= $this->readEncodedChars(1, $first);
}
return \quoted_printable_decode($block);
}
/**
* Reads up to $length characters, appends them to the passed $str string,
* and returns the total number of characters read.
*
* -1 is returned if there are no more bytes to read.
*/
private function readRawDecodeAndAppend(int $length, string &$str) : int
{
$block = $this->stream->read($length);
if ($block === '') {
return -1;
}
$decoded = $this->decodeBlock($block);
$count = \strlen($decoded);
$str .= $decoded;
return $count;
}
/**
* Reads up to $length decoded bytes from the underlying quoted-printable
* encoded stream and returns them.
*
* @param int $length
*/
public function read($length) : string
{
// let Guzzle decide what to do.
if ($length <= 0 || $this->eof()) {
return $this->stream->read($length);
}
$count = 0;
$bytes = '';
while ($count < $length) {
$nRead = $this->readRawDecodeAndAppend($length - $count, $bytes);
if ($nRead === -1) {
break;
}
$this->position += $nRead;
$count += $nRead;
}
return $bytes;
}
/**
* Writes the passed string to the underlying stream after encoding it as
* quoted-printable.
*
* Note that reading and writing to the same stream without rewinding is not
* supported.
*
* @param string $string
*
* @return int the number of bytes written
*/
public function write($string) : int
{
$encodedLine = \quoted_printable_encode($this->lastLine);
$lineAndString = \rtrim(\quoted_printable_encode($this->lastLine . $string), "\r\n");
$write = \substr($lineAndString, \strlen($encodedLine));
$this->stream->write($write);
$written = \strlen($string);
$this->position += $written;
$lpos = \strrpos($lineAndString, "\n");
$lastLine = $lineAndString;
if ($lpos !== false) {
$lastLine = \substr($lineAndString, $lpos + 1);
}
$this->lastLine = \quoted_printable_decode($lastLine);
return $written;
}
/**
* Writes out a final CRLF if the current line isn't empty.
*/
private function beforeClose() : void
{
if ($this->isWritable() && $this->lastLine !== '') {
$this->stream->write("\r\n");
$this->lastLine = '';
}
}
/**
* @inheritDoc
*/
public function close() : void
{
$this->beforeClose();
$this->stream->close();
}
/**
* @inheritDoc
*/
public function detach()
{
$this->beforeClose();
$this->stream->detach();
return null;
}
}
@@ -0,0 +1,192 @@
<?php
/**
* This file is part of the ZBateson\StreamDecorators project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\StreamDecorators;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
/**
* Maintains an internal 'read' position, and seeks to it before reading, then
* seeks back to the original position of the underlying stream after reading if
* the attached stream supports seeking.
*
* Although copied form LimitStream, it's not inherited from it since $offset
* and $limit are set to private on LimitStream, and most other functions are
* re-implemented anyway. This also decouples the implementation from upstream
* changes.
*/
class SeekingLimitStream implements StreamInterface
{
use StreamDecoratorTrait;
/** @var int Offset to start reading from */
private int $offset;
/** @var int Limit the number of bytes that can be read */
private int $limit;
/**
* @var int Number of bytes written, and importantly, if non-zero, writes a
* final $lineEnding on close (and so maintained instead of using
* tell() directly)
*/
private int $position = 0;
/**
* @var StreamInterface $stream
*/
private StreamInterface $stream;
/**
* @param StreamInterface $stream Stream to wrap
* @param int $limit Total number of bytes to allow to be read
* from the stream. Pass -1 for no limit.
* @param int $offset Position to seek to before reading (only
* works on seekable streams).
*/
public function __construct(StreamInterface $stream, int $limit = -1, int $offset = 0)
{
$this->stream = $stream;
$this->setLimit($limit);
$this->setOffset($offset);
}
/**
* Returns the current relative read position of this stream subset.
*/
public function tell() : int
{
return $this->position;
}
/**
* Returns the size of the limited subset of data, or null if the wrapped
* stream returns null for getSize.
*/
public function getSize() : ?int
{
$size = $this->stream->getSize();
if ($size === null) {
// this shouldn't happen on a seekable stream I don't think...
$pos = $this->stream->tell();
$this->stream->seek(0, SEEK_END);
$size = $this->stream->tell();
$this->stream->seek($pos);
}
if ($this->limit === -1) {
return $size - $this->offset;
}
return \min([$this->limit, $size - $this->offset]);
}
/**
* Returns true if the current read position is at the end of the limited
* stream
*/
public function eof() : bool
{
$size = $this->limit;
if ($size === -1) {
$size = $this->getSize();
}
return ($this->position >= $size);
}
/**
* Ensures the seek position specified is within the stream's bounds, and
* sets the internal position pointer (doesn't actually seek).
*/
private function doSeek(int $pos) : void
{
if ($this->limit !== -1) {
$pos = \min([$pos, $this->limit]);
}
$this->position = \max([0, $pos]);
}
/**
* Seeks to the passed position within the confines of the limited stream's
* bounds.
*
* For SeekingLimitStream, no actual seek is performed on the underlying
* wrapped stream. Instead, an internal pointer is set, and the stream is
* 'seeked' on read operations
*
* @param int $offset
* @param int $whence
*/
public function seek($offset, $whence = SEEK_SET) : void
{
$pos = $offset;
switch ($whence) {
case SEEK_CUR:
$pos = $this->position + $offset;
break;
case SEEK_END:
$pos = $this->limit + $offset;
break;
default:
break;
}
$this->doSeek($pos);
}
/**
* Sets the offset to start reading from the wrapped stream.
*/
public function setOffset(int $offset) : void
{
$this->offset = $offset;
$this->position = 0;
}
/**
* Sets the length of the stream to the passed $limit.
*/
public function setLimit(int $limit) : void
{
$this->limit = $limit;
}
/**
* Seeks to the current position and reads up to $length bytes, or less if
* it would result in reading past $this->limit
*/
public function seekAndRead(int $length) : string
{
$this->stream->seek($this->offset + $this->position);
if ($this->limit !== -1) {
$length = \min($length, $this->limit - $this->position);
if ($length <= 0) {
return '';
}
}
return $this->stream->read($length);
}
/**
* Reads from the underlying stream after seeking to the position within the
* bounds set for this limited stream. After reading, the wrapped stream is
* 'seeked' back to its position prior to the call to read().
*
* @param int $length
*/
public function read($length) : string
{
$pos = $this->stream->tell();
$ret = $this->seekAndRead($length);
$this->position += \strlen($ret);
$this->stream->seek($pos);
if ($this->limit !== -1 && $this->position > $this->limit) {
$ret = \substr($ret, 0, -($this->position - $this->limit));
$this->position = $this->limit;
}
return $ret;
}
}
@@ -0,0 +1,31 @@
<?php
/**
* This file is part of the ZBateson\StreamDecorators project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\StreamDecorators;
use Psr\Http\Message\StreamInterface;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
/**
* Calling tell() always returns 0. Used by DecoratedCachingStream so a
* CachingStream can use a BufferedStream, because BufferedStream throws an
* exception in tell().
*/
class TellZeroStream implements StreamInterface
{
use StreamDecoratorTrait;
/**
* @var StreamInterface
*/
private StreamInterface $stream;
public function tell() : int
{
return 0;
}
}
+315
View File
@@ -0,0 +1,315 @@
<?php
/**
* This file is part of the ZBateson\StreamDecorators project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\StreamDecorators;
use GuzzleHttp\Psr7\BufferStream;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
/**
* GuzzleHttp\Psr7 stream decoder extension for UU-Encoded streams.
*
* 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 UUStream implements StreamInterface
{
use StreamDecoratorTrait;
/**
* @var string name of the UUEncoded file
*/
protected $filename = null;
/**
* @var BufferStream of read and decoded bytes
*/
private $buffer;
/**
* @var string remainder of write operation if the bytes didn't align to 3
* bytes
*/
private $remainder = '';
/**
* @var int read/write position
*/
private $position = 0;
/**
* @var bool set to true when 'write' is called
*/
private $isWriting = false;
/**
* @var StreamInterface $stream
*/
private $stream;
/**
* @param StreamInterface $stream Stream to decorate
* @param string $filename optional file name
*/
public function __construct(StreamInterface $stream, ?string $filename = null)
{
$this->stream = $stream;
$this->filename = $filename;
$this->buffer = new BufferStream();
}
/**
* Overridden to return the position in the target encoding.
*/
public function tell() : int
{
return $this->position;
}
/**
* Returns null, getSize isn't supported
*
* @return null
*/
public function getSize() : ?int
{
return null;
}
/**
* Not supported.
*
* @param int $offset
* @param int $whence
* @throws RuntimeException
*/
public function seek($offset, $whence = SEEK_SET) : void
{
throw new RuntimeException('Cannot seek a UUStream');
}
/**
* Overridden to return false
*/
public function isSeekable() : bool
{
return false;
}
/**
* Finds the next end-of-line character to ensure a line isn't broken up
* while buffering.
*/
private function readToEndOfLine(int $length) : string
{
$str = $this->stream->read($length);
if ($str === '') {
return $str;
}
while (\substr($str, -1) !== "\n") {
$chr = $this->stream->read(1);
if ($chr === '') {
break;
}
$str .= $chr;
}
return $str;
}
/**
* Removes invalid characters from a uuencoded string, and 'BEGIN' and 'END'
* line headers and footers from the passed string before returning it.
*/
private function filterAndDecode(string $str) : string
{
$ret = \str_replace("\r", '', $str);
$ret = \preg_replace('/[^\x21-\xf5`\n]/', '`', $ret);
if ($this->position === 0) {
$matches = [];
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*$/im', '', $ret);
} else {
$ret = \preg_replace('/^\s*end\s*$/im', '', $ret);
}
return \convert_uudecode(\trim($ret));
}
/**
* Buffers bytes into $this->buffer, removing uuencoding headers and footers
* and decoding them.
*/
private function fillBuffer(int $length) : void
{
// 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
while ($this->buffer->getSize() < $length) {
$read = $this->readToEndOfLine(5040);
if ($read === '') {
break;
}
$this->buffer->write($this->filterAndDecode($read));
}
}
/**
* Returns true if the end of stream has been reached.
*/
public function eof() : bool
{
return ($this->buffer->eof() && $this->stream->eof());
}
/**
* Attempts to read $length bytes after decoding them, and returns them.
*
* @param int $length
*/
public function read($length) : string
{
// let Guzzle decide what to do.
if ($length <= 0 || $this->eof()) {
return $this->stream->read($length);
}
$this->fillBuffer($length);
$read = $this->buffer->read($length);
$this->position += \strlen($read);
return $read;
}
/**
* Writes the 'begin' UU header line.
*/
private function writeUUHeader() : void
{
$filename = (empty($this->filename)) ? 'null' : $this->filename;
$this->stream->write("begin 666 $filename");
}
/**
* Writes the '`' and 'end' UU footer lines.
*/
private function writeUUFooter() : void
{
$this->stream->write("\r\n`\r\nend\r\n");
}
/**
* Writes the passed bytes to the underlying stream after encoding them.
*/
private function writeEncoded(string $bytes) : void
{
$encoded = \preg_replace('/\r\n|\r|\n/', "\r\n", \rtrim(\convert_uuencode($bytes)));
// removes ending '`' line
$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.
*/
private function handleRemainder(string $string) : 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;
}
/**
* Writes the passed string to the underlying stream after encoding it.
*
* 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 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) : int
{
$this->isWriting = true;
if ($this->position === 0) {
$this->writeUUHeader();
}
$write = $this->handleRemainder($string);
if ($write !== '') {
$this->writeEncoded($write);
}
$written = \strlen($string);
$this->position += $written;
return $written;
}
/**
* Returns the filename set in the UUEncoded header (or null)
*/
public function getFilename() : string
{
return $this->filename;
}
/**
* Sets the UUEncoded header file name written in the 'begin' header line.
*/
public function setFilename(string $filename) : void
{
$this->filename = $filename;
}
/**
* Writes out any remaining bytes and the UU footer.
*/
private function beforeClose() : void
{
if (!$this->isWriting) {
return;
}
if ($this->remainder !== '') {
$this->writeEncoded($this->remainder);
}
$this->remainder = '';
$this->isWriting = false;
$this->writeUUFooter();
}
/**
* @inheritDoc
*/
public function close() : void
{
$this->beforeClose();
$this->stream->close();
}
/**
* @inheritDoc
*/
public function detach()
{
$this->beforeClose();
$this->stream->detach();
return null;
}
}