Stream detects read/write more accurately; fix issues after detach()

This commit is contained in:
PJ Dietz 2020-08-09 10:55:37 -04:00
parent fe780e6b92
commit a7b08ad8a3
2 changed files with 194 additions and 16 deletions

View File

@ -6,7 +6,10 @@ use Psr\Http\Message\StreamInterface;
class Stream implements StreamInterface
{
/** @var resource */
private const READABLE_MODES = ['r', 'r+', 'w+', 'a+', 'x+', 'c+'];
private const WRITABLE_MODES = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
/** @var resource|null */
private $resource;
/**
@ -46,18 +49,16 @@ class Stream implements StreamInterface
*/
public function __toString()
{
$string = "";
try {
if ($this->isSeekable()) {
rewind($this->resource);
$this->rewind();
}
$string = $this->getContents();
// @codeCoverageIgnoreStart
return $this->getContents();
} catch (\Exception $e) {
// @codeCoverageIgnoreEnd
// Silence exceptions in order to conform with PHP's string casting operations.
// Silence exceptions in order to conform with PHP's string casting
// operations.
return '';
}
return $string;
}
/**
@ -67,6 +68,10 @@ class Stream implements StreamInterface
*/
public function close()
{
if ($this->resource === null) {
return;
}
$resource = $this->resource;
fclose($resource);
$this->resource = null;
@ -93,6 +98,10 @@ class Stream implements StreamInterface
*/
public function getSize()
{
if ($this->resource === null) {
return null;
}
$statistics = fstat($this->resource);
if ($statistics && $statistics["size"]) {
return $statistics["size"];
@ -108,6 +117,10 @@ class Stream implements StreamInterface
*/
public function tell()
{
if ($this->resource === null) {
throw new \RuntimeException("Unable to retrieve current position of detached stream.");
}
$position = ftell($this->resource);
if ($position === false) {
throw new \RuntimeException("Unable to retrieve current position of file pointer.");
@ -122,6 +135,10 @@ class Stream implements StreamInterface
*/
public function eof()
{
if ($this->resource === null) {
return true;
}
return feof($this->resource);
}
@ -132,6 +149,10 @@ class Stream implements StreamInterface
*/
public function isSeekable()
{
if ($this->resource === null) {
return false;
}
return $this->getMetadata("seekable") == 1;
}
@ -149,6 +170,10 @@ class Stream implements StreamInterface
*/
public function seek($offset, $whence = SEEK_SET)
{
if ($this->resource === null) {
throw new \RuntimeException("Unable to seek detached stream.");
}
$result = -1;
if ($this->isSeekable()) {
$result = fseek($this->resource, $offset, $whence);
@ -170,12 +195,16 @@ class Stream implements StreamInterface
*/
public function rewind()
{
if ($this->resource === null) {
throw new \RuntimeException("Unable to seek detached stream.");
}
$result = false;
if ($this->isSeekable()) {
$result = rewind($this->resource);
}
if ($result === false) {
throw new \RuntimeException("Unable to seek to position.");
throw new \RuntimeException("Unable to rewind.");
}
}
@ -186,8 +215,12 @@ class Stream implements StreamInterface
*/
public function isWritable()
{
$mode = $this->getMetadata("mode");
return $mode[0] !== "r" || strpos($mode, "+") !== false;
if ($this->resource === null) {
return false;
}
$mode = $this->getBasicMode();
return in_array($mode, self::WRITABLE_MODES);
}
/**
@ -199,6 +232,10 @@ class Stream implements StreamInterface
*/
public function write($string)
{
if ($this->resource === null) {
throw new \RuntimeException("Unable to write to detached stream.");
}
$result = false;
if ($this->isWritable()) {
$result = fwrite($this->resource, $string);
@ -216,8 +253,12 @@ class Stream implements StreamInterface
*/
public function isReadable()
{
$mode = $this->getMetadata("mode");
return strpos($mode, "r") !== false || strpos($mode, "+") !== false;
if ($this->resource === null) {
return false;
}
$mode = $this->getBasicMode();
return in_array($mode, self::READABLE_MODES);
}
/**
@ -232,6 +273,10 @@ class Stream implements StreamInterface
*/
public function read($length)
{
if ($this->resource === null) {
throw new \RuntimeException("Unable to read to detached stream.");
}
$result = false;
if ($this->isReadable()) {
$result = fread($this->resource, $length);
@ -251,6 +296,10 @@ class Stream implements StreamInterface
*/
public function getContents()
{
if ($this->resource === null) {
throw new \RuntimeException("Unable to read to detached stream.");
}
$result = false;
if ($this->isReadable()) {
$result = stream_get_contents($this->resource);
@ -268,13 +317,17 @@ class Stream implements StreamInterface
* stream_get_meta_data() function.
*
* @link http://php.net/manual/en/function.stream-get-meta-data.php
* @param string $key Specific metadata to retrieve.
* @param string|null $key Specific metadata to retrieve.
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata($key = null)
{
if ($this->resource === null) {
return null;
}
$metadata = stream_get_meta_data($this->resource);
if ($key === null) {
return $metadata;
@ -282,4 +335,14 @@ class Stream implements StreamInterface
return $metadata[$key];
}
}
/**
* @return string Mode for the resource reduced to only the characters
* r, w, a, x, c, and + needed to determine readable and writeable status.
*/
private function getBasicMode()
{
$mode = $this->getMetadata('mode') ?? '';
return preg_replace('/[^rwaxc+]/', '', $mode);
}
}

View File

@ -13,14 +13,14 @@ class StreamTest extends TestCase
private $resourceDevNull;
private $content = "Hello, world!";
public function setUp(): void
protected function setUp(): void
{
$this->resource = fopen("php://memory", "w+");
$this->resourceDevNull = fopen("/dev/null", "r");
fwrite($this->resource, $this->content);
}
public function tearDown(): void
protected function tearDown(): void
{
if (is_resource($this->resource)) {
fclose($this->resource);
@ -269,4 +269,119 @@ class StreamTest extends TestCase
["c+", true, true]
];
}
// -------------------------------------------------------------------------
// After Detach
public function testAfterDetachToStringReturnsEmptyString(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->assertEquals('', (string) $stream);
}
public function testAfterDetachCloseDoesNothing(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$stream->close();
$this->assertTrue(true);
}
public function testAfterDetachDetachReturnsNull(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->assertNull($stream->detach());
}
public function testAfterDetachGetSizeReturnsNull(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->assertNull($stream->getSize());
}
public function testAfterDetachTellThrowsRuntimeException(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->expectException(RuntimeException::class);
$stream->tell();
}
public function testAfterDetachEofReturnsTrue(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->assertTrue($stream->eof());
}
public function testAfterDetachIsSeekableReturnsFalse(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->assertFalse($stream->isSeekable());
}
public function testAfterDetachSeekThrowsRuntimeException(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->expectException(RuntimeException::class);
$stream->seek(0);
}
public function testAfterDetachRewindThrowsRuntimeException(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->expectException(RuntimeException::class);
$stream->rewind();
}
public function testAfterDetachIsWritableReturnsFalse(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->assertFalse($stream->isWritable());
}
public function testAfterDetachWriteThrowsRuntimeException(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->expectException(RuntimeException::class);
$stream->write('bork');
}
public function testAfterDetachIsReadableReturnsFalse(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->assertFalse($stream->isReadable());
}
public function testAfterDetachReadThrowsRuntimeException(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->expectException(RuntimeException::class);
$stream->read(10);
}
public function testAfterDetachGetContentsThrowsRuntimeException(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->expectException(RuntimeException::class);
$stream->getContents();
}
public function testAfterDetachGetMetadataReturnsNull(): void
{
$stream = new Stream($this->resource);
$stream->detach();
$this->assertNull($stream->getMetadata());
}
}