Skip to content

Commit

Permalink
Safely get stream contents (#515)
Browse files Browse the repository at this point in the history
  • Loading branch information
GrahamCampbell authored Jun 8, 2022
1 parent a07b9fd commit 3d12ad5
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 7 deletions.
10 changes: 10 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ parameters:
count: 1
path: src/UriResolver.php

-
message: "#^Method GuzzleHttp\\\\Psr7\\\\Utils\\:\\:tryStreamGetContents\\(\\) should return string but returns string\\|false\\.$#"
count: 1
path: src/Utils.php

-
message: "#^Offset 'uri' on array\\{timed_out\\: bool, blocked\\: bool, eof\\: bool, unread_bytes\\: int, stream_type\\: string, wrapper_type\\: string, wrapper_data\\: mixed, mode\\: string, \\.\\.\\.\\} on left side of \\?\\? always exists and is not nullable\\.$#"
count: 1
Expand All @@ -325,6 +330,11 @@ parameters:
count: 1
path: src/Utils.php

-
message: "#^Variable \\$contents might not be defined\\.$#"
count: 1
path: src/Utils.php

-
message: "#^Variable \\$handle might not be defined\\.$#"
count: 1
Expand Down
9 changes: 7 additions & 2 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,15 @@
</TypeDoesNotContainType>
</file>
<file src="src/Utils.php">
<MissingDocblockType occurrences="1">
<FalsableReturnStatement occurrences="1">
<code>$contents</code>
</FalsableReturnStatement>
<MissingDocblockType occurrences="2">
<code>throw $ex;</code>
<code>throw $ex;</code>
</MissingDocblockType>
<PossiblyUndefinedVariable occurrences="1">
<PossiblyUndefinedVariable occurrences="2">
<code>$contents</code>
<code>$handle</code>
</PossiblyUndefinedVariable>
</file>
Expand Down
8 changes: 3 additions & 5 deletions src/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,11 @@ public function getContents(): string
throw new \RuntimeException('Stream is detached');
}

$contents = stream_get_contents($this->stream);

if ($contents === false) {
throw new \RuntimeException('Unable to read stream contents');
if (!$this->readable) {
throw new \RuntimeException('Cannot read from non-readable stream');
}

return $contents;
return Utils::tryStreamGetContents($this->stream);
}

public function close(): void
Expand Down
47 changes: 47 additions & 0 deletions src/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,53 @@ public static function tryFopen(string $filename, string $mode)
return $handle;
}

/**
* Safely gets the contents of a given stream.
*
* When stream_get_contents fails, PHP normally raises a warning. This
* function adds an error handler that checks for errors and throws an
* exception instead.
*
* @param resource $stream
*
* @throws \RuntimeException if the stream cannot be read
*/
public static function tryStreamGetContents($stream): string
{
$ex = null;
set_error_handler(static function (int $errno, string $errstr) use (&$ex): bool {
$ex = new \RuntimeException(sprintf(
'Unable to read stream contents: %s',
$errstr
));

return true;
});

try {
/** @var string|false $contents */
$contents = stream_get_contents($stream);

if ($contents === false) {
$ex = new \RuntimeException('Unable to read stream contents');
}
} catch (\Throwable $e) {
$ex = new \RuntimeException(sprintf(
'Unable to read stream contents: %s',
$e->getMessage()
), 0, $e);
}

restore_error_handler();

if ($ex) {
/** @var $ex \RuntimeException */
throw $ex;
}

return $contents;
}

/**
* Returns a UriInterface for the given value.
*
Expand Down
18 changes: 18 additions & 0 deletions tests/StreamTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,24 @@ public function testReadOnlyStreamIsNotWritable(): void

$stream->close();
}

public function testCannotReadUnreadableStream(): void
{
$r = fopen(tempnam(sys_get_temp_dir(), 'guzzle-psr7-'), 'w');
$stream = new Stream($r);

$stream->write("Hello world!!");

$stream->seek(0);

$this->expectException(\RuntimeException::class);

try {
$stream->getContents();
} finally {
$stream->close();
}
}
}

namespace GuzzleHttp\Psr7;
Expand Down
29 changes: 29 additions & 0 deletions tests/UtilsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,35 @@ public function testThrowsExceptionNotValueError(): void
Psr7\Utils::tryFopen('', 'r');
}

/**
* @requires PHP 7.4
*/
public function testGetsContentsThrowExceptionWhenNotReadable(): void
{
$r = fopen(tempnam(sys_get_temp_dir(), 'guzzle-psr7-'), 'w');
fwrite($r, 'hello world!');

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Unable to read stream contents');

try {
Psr7\Utils::tryStreamGetContents($r);
} finally {
fclose($r);
}
}

public function testGetsContentsThrowExceptionWhenCLosed(): void
{
$r = fopen(tempnam(sys_get_temp_dir(), 'guzzle-psr7-'), 'r+');
fclose($r);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Unable to read stream contents');

Psr7\Utils::tryStreamGetContents($r);
}

public function testCreatesUriForValue(): void
{
self::assertInstanceOf('GuzzleHttp\Psr7\Uri', Psr7\Utils::uriFor('/foo'));
Expand Down

0 comments on commit 3d12ad5

Please sign in to comment.