Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ final class FileReadTrapStreamWrapper

public static ?string $autoloadLocatedFile = null;

private bool $readFromFile = false;

private int $seekPosition = 0;
/** @var resource|null */
private $file = null;

/**
* @param string[] $streamWrapperProtocols
Expand Down Expand Up @@ -88,29 +87,43 @@ public static function withStreamWrapperOverride(
*/
public function stream_open($path, $mode, $options, &$openedPath): bool
{
self::$autoloadLocatedFile = $path;
$this->readFromFile = false;
$this->seekPosition = 0;
if (self::$autoloadLocatedFile === null) {
//We want to capture the first file only. Since we allow the autoloading to continue, this will be called
//multiple times if loading the class caused other files to be loaded too.
self::$autoloadLocatedFile = $path;
}
return $this->runUnwrapped(function () use ($path, $mode, $options) {
if (($options & STREAM_REPORT_ERRORS) !== 0) {
$file = fopen($path, $mode, ($options & STREAM_USE_PATH) !== 0);
} else {
$file = @fopen($path, $mode, ($options & STREAM_USE_PATH) !== 0);
}
if ($file !== false) {
$this->file = $file;
return true;
}

return true;
return false;
});
}

/**
* Since we allow our wrapper's stream_open() to succeed, we need to
* simulate a successful read so autoloaders with require() don't explode.
*
* @param int $count
* @param 0|positive-int $count
*
* @return string
* @return string|bool
*/
public function stream_read($count): string
public function stream_read($count)
{
$this->readFromFile = true;

// Dummy return value that is also valid PHP for require(). We'll read
// and process the file elsewhere, so it's OK to provide dummy data for
// this read.
return '';
$file = $this->file;
if ($file === null) {
return false;
}
return $this->runUnwrapped(static function () use ($file, $count) {
return fread($file, $count);
});
}

/**
Expand All @@ -121,7 +134,11 @@ public function stream_read($count): string
*/
public function stream_close(): void
{
// no op
if ($this->file === null) {
return;
}

fclose($this->file);
}

/**
Expand All @@ -134,11 +151,13 @@ public function stream_close(): void
*/
public function stream_stat()
{
if (self::$autoloadLocatedFile === null) {
$file = $this->file;
if ($file === null) {
return false;
}

return $this->url_stat(self::$autoloadLocatedFile, STREAM_URL_STAT_QUIET);
return $this->runUnwrapped(static function () use ($file) {
return fstat($file);
});
}

/**
Expand All @@ -159,26 +178,13 @@ public function stream_stat()
*/
public function url_stat($path, $flags)
{
if (self::$registeredStreamWrapperProtocols === null) {
throw new \PHPStan\ShouldNotHappenException(self::class . ' not registered: cannot operate. Do not call this method directly.');
}

foreach (self::$registeredStreamWrapperProtocols as $protocol) {
stream_wrapper_restore($protocol);
}

if (($flags & STREAM_URL_STAT_QUIET) !== 0) {
$result = @stat($path);
} else {
$result = stat($path);
}

foreach (self::$registeredStreamWrapperProtocols as $protocol) {
stream_wrapper_unregister($protocol);
stream_wrapper_register($protocol, self::class);
}
return $this->runUnwrapped(static function () use ($path, $flags) {
if (($flags & STREAM_URL_STAT_QUIET) !== 0) {
return @stat($path);
}

return $result;
return stat($path);
});
}

/**
Expand All @@ -188,17 +194,17 @@ public function url_stat($path, $flags)
*/
public function stream_eof(): bool
{
return $this->readFromFile;
return $this->file === null || feof($this->file);
}

public function stream_flush(): bool
{
return true;
return $this->file !== null && fflush($this->file);
}

public function stream_tell(): int
{
return $this->seekPosition;
return $this->file !== null ? (int) ftell($this->file) : 0;
}

/**
Expand All @@ -208,26 +214,13 @@ public function stream_tell(): int
*/
public function stream_seek($offset, $whence): bool
{
switch ($whence) {
// Behavior is the same for a zero-length file
case SEEK_SET:
case SEEK_END:
if ($offset < 0) {
return false;
}
$this->seekPosition = $offset;
return true;

case SEEK_CUR:
if ($offset < 0) {
return false;
}
$this->seekPosition += $offset;
return true;

default:
return false;
$file = $this->file;
if ($file === null) {
return false;
}
return $this->runUnwrapped(static function () use ($file, $offset, $whence): bool {
return fseek($file, $offset, $whence) === 0;
});
}

/**
Expand All @@ -241,4 +234,29 @@ public function stream_set_option($option, $arg1, $arg2): bool
return false;
}

/**
* @phpstan-template TReturn
* @phpstan-param callable() : TReturn $c
* @phpstan-return TReturn
*/
private function runUnwrapped(callable $c)
{
if (self::$registeredStreamWrapperProtocols === null) {
throw new \PHPStan\ShouldNotHappenException(self::class . ' not registered: cannot operate. Do not call this method directly.');
}

foreach (self::$registeredStreamWrapperProtocols as $protocol) {
stream_wrapper_restore($protocol);
}

$result = $c();

foreach (self::$registeredStreamWrapperProtocols as $protocol) {
stream_wrapper_unregister($protocol);
stream_wrapper_register($protocol, self::class);
}

return $result;
}

}