diff --git a/Slim/Http/Stream.php b/Slim/Http/Stream.php index 97de9ac00..62c956210 100644 --- a/Slim/Http/Stream.php +++ b/Slim/Http/Stream.php @@ -19,6 +19,13 @@ */ class Stream implements StreamInterface { + /** + * Bit mask to determine if the stream is a pipe + * + * This is octal as per header stat.h + */ + const FSTAT_MODE_S_IFIFO = 0010000; + /** * Resource modes * @@ -72,6 +79,13 @@ class Stream implements StreamInterface */ protected $size; + /** + * Is this stream a pipe? + * + * @var bool + */ + protected $isPipe; + /** * Create a new Stream. * @@ -158,6 +172,7 @@ public function detach() $this->writable = null; $this->seekable = null; $this->size = null; + $this->isPipe = null; return $oldResource; } @@ -196,7 +211,11 @@ public function __toString() public function close() { if ($this->isAttached() === true) { - fclose($this->stream); + if ($this->isPipe()) { + pclose($this->stream); + } else { + fclose($this->stream); + } } $this->detach(); @@ -211,7 +230,7 @@ public function getSize() { if (!$this->size && $this->isAttached() === true) { $stats = fstat($this->stream); - $this->size = isset($stats['size']) ? $stats['size'] : null; + $this->size = isset($stats['size']) && !$this->isPipe() ? $stats['size'] : null; } return $this->size; @@ -226,7 +245,7 @@ public function getSize() */ public function tell() { - if (!$this->isAttached() || ($position = ftell($this->stream)) === false) { + if (!$this->isAttached() || ($position = ftell($this->stream)) === false || $this->isPipe()) { throw new RuntimeException('Could not get the position of the pointer in stream'); } @@ -251,13 +270,17 @@ public function eof() public function isReadable() { if ($this->readable === null) { - $this->readable = false; - if ($this->isAttached()) { - $meta = $this->getMetadata(); - foreach (self::$modes['readable'] as $mode) { - if (strpos($meta['mode'], $mode) === 0) { - $this->readable = true; - break; + if ($this->isPipe()) { + $this->readable = true; + } else { + $this->readable = false; + if ($this->isAttached()) { + $meta = $this->getMetadata(); + foreach (self::$modes['readable'] as $mode) { + if (strpos($meta['mode'], $mode) === 0) { + $this->readable = true; + break; + } } } } @@ -300,7 +323,7 @@ public function isSeekable() $this->seekable = false; if ($this->isAttached()) { $meta = $this->getMetadata(); - $this->seekable = $meta['seekable']; + $this->seekable = !$this->isPipe() && $meta['seekable']; } } @@ -406,4 +429,22 @@ public function getContents() return $contents; } + + /** + * Returns whether or not the stream is a pipe. + * + * @return bool + */ + public function isPipe() + { + if ($this->isPipe === null) { + $this->isPipe = false; + if ($this->isAttached()) { + $mode = fstat($this->stream)['mode']; + $this->isPipe = ($mode & self::FSTAT_MODE_S_IFIFO) !== 0; + } + } + + return $this->isPipe; + } } diff --git a/tests/Http/StreamTest.php b/tests/Http/StreamTest.php new file mode 100644 index 000000000..084d07dcf --- /dev/null +++ b/tests/Http/StreamTest.php @@ -0,0 +1,158 @@ +pipeFh != null) { + stream_get_contents($this->pipeFh); // prevent broken pipe error message + } + } + + /** + * @covers Slim\Http\Stream::isPipe + */ + public function testIsPipe() + { + $this->openPipeStream(); + + $this->assertTrue($this->pipeStream->isPipe()); + + $this->pipeStream->detach(); + $this->assertFalse($this->pipeStream->isPipe()); + + $fhFile = fopen(__FILE__, 'r'); + $fileStream = new Stream($fhFile); + $this->assertFalse($fileStream->isPipe()); + } + + /** + * @covers Slim\Http\Stream::isReadable + */ + public function testIsPipeReadable() + { + $this->openPipeStream(); + + $this->assertTrue($this->pipeStream->isReadable()); + } + + /** + * @covers Slim\Http\Stream::isSeekable + */ + public function testPipeIsNotSeekable() + { + $this->openPipeStream(); + + $this->assertFalse($this->pipeStream->isSeekable()); + } + + /** + * @covers Slim\Http\Stream::seek + * @expectedException \RuntimeException + */ + public function testCannotSeekPipe() + { + $this->openPipeStream(); + + $this->pipeStream->seek(0); + } + + /** + * @covers Slim\Http\Stream::tell + * @expectedException \RuntimeException + */ + public function testCannotTellPipe() + { + $this->openPipeStream(); + + $this->pipeStream->tell(); + } + + /** + * @covers Slim\Http\Stream::rewind + * @expectedException \RuntimeException + */ + public function testCannotRewindPipe() + { + $this->openPipeStream(); + + $this->pipeStream->rewind(); + } + + /** + * @covers Slim\Http\Stream::getSize + */ + public function testPipeGetSizeYieldsNull() + { + $this->openPipeStream(); + + $this->assertNull($this->pipeStream->getSize()); + } + + /** + * @covers Slim\Http\Stream::close + */ + public function testClosePipe() + { + $this->openPipeStream(); + + stream_get_contents($this->pipeFh); // prevent broken pipe error message + $this->pipeStream->close(); + $this->pipeFh = null; + + $this->assertFalse($this->pipeStream->isPipe()); + } + + /** + * @covers Slim\Http\Stream::__toString + */ + public function testPipeToString() + { + $this->openPipeStream(); + + $this->assertSame('', (string) $this->pipeStream); + } + + /** + * @covers Slim\Http\Stream::getContents + */ + + public function testPipeGetContents() + { + $this->openPipeStream(); + + $contents = trim($this->pipeStream->getContents()); + $this->assertSame('12', $contents); + } + + /** + * Opens the pipe stream + * + * @see StreamTest::pipeStream + */ + private function openPipeStream() + { + $this->pipeFh = popen('echo 12', 'r'); + $this->pipeStream = new Stream($this->pipeFh); + } +}