Skip to content

Instantly share code, notes, and snippets.

@milo
Created September 10, 2018 07:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milo/c13fe314c486451d2c545b923e73622f to your computer and use it in GitHub Desktop.
Save milo/c13fe314c486451d2c545b923e73622f to your computer and use it in GitHub Desktop.
php7.3.0beta3 streamWrapper issue
<?php
declare(strict_types=1);
class FileMock
{
private const PROTOCOL = 'mock';
/** @var string[] */
public static $files = [];
/** @var string */
private $content;
/** @var int */
private $readingPos;
/** @var int */
private $writingPos;
/** @var bool */
private $appendMode;
/** @var bool */
private $isReadable;
/** @var bool */
private $isWritable;
/**
* @return string file name
*/
public static function create(string $content = '', string $extension = null): string
{
self::register();
static $id;
$name = self::PROTOCOL . '://' . (++$id) . '.' . $extension;
self::$files[$name] = $content;
return $name;
}
public static function register(): void
{
if (!in_array(self::PROTOCOL, stream_get_wrappers(), true)) {
stream_wrapper_register(self::PROTOCOL, __CLASS__);
}
}
public function stream_open(string $path, string $mode): bool
{
if (!preg_match('#^([rwaxc]).*?(\+)?#', $mode, $m)) {
// Windows: failed to open stream: Bad file descriptor
// Linux: failed to open stream: Illegal seek
$this->warning("failed to open stream: Invalid mode '$mode'");
return false;
} elseif ($m[1] === 'x' && isset(self::$files[$path])) {
$this->warning('failed to open stream: File exists');
return false;
} elseif ($m[1] === 'r' && !isset(self::$files[$path])) {
$this->warning('failed to open stream: No such file or directory');
return false;
} elseif ($m[1] === 'w' || $m[1] === 'x') {
self::$files[$path] = '';
}
$this->content = &self::$files[$path];
$this->content = (string) $this->content;
$this->appendMode = $m[1] === 'a';
$this->readingPos = 0;
$this->writingPos = $this->appendMode ? strlen($this->content) : 0;
$this->isReadable = isset($m[2]) || $m[1] === 'r';
$this->isWritable = isset($m[2]) || $m[1] !== 'r';
return true;
}
public function stream_read(int $length): string
{
if (!$this->isReadable) {
return '';
}
$result = substr($this->content, $this->readingPos, $length);
$this->readingPos += strlen($result);
$this->writingPos += $this->appendMode ? 0 : strlen($result);
return $result;
}
public function stream_write(string $data): int
{
if (!$this->isWritable) {
return 0;
}
$length = strlen($data);
$this->content = str_pad($this->content, $this->writingPos, "\x00");
$this->content = substr_replace($this->content, $data, $this->writingPos, $length);
$this->readingPos += $length;
$this->writingPos += $length;
return $length;
}
public function stream_tell(): int
{
return $this->readingPos;
}
public function stream_eof(): bool
{
return $this->readingPos >= strlen($this->content);
}
public function stream_seek(int $offset, int $whence): bool
{
if ($whence === SEEK_CUR) {
$offset += $this->readingPos;
} elseif ($whence === SEEK_END) {
$offset += strlen($this->content);
}
if ($offset >= 0) {
$this->readingPos = $offset;
$this->writingPos = $this->appendMode ? $this->writingPos : $offset;
return true;
} else {
return false;
}
}
public function stream_truncate(int $size): bool
{
if (!$this->isWritable) {
return false;
}
$this->content = substr(str_pad($this->content, $size, "\x00"), 0, $size);
$this->writingPos = $this->appendMode ? $size : $this->writingPos;
return true;
}
public function stream_stat(): array
{
return ['mode' => 0100666, 'size' => strlen($this->content)];
}
private function warning(string $message): void
{
$bt = debug_backtrace(0, 3);
if (isset($bt[2]['function'])) {
$message = $bt[2]['function'] . '(' . @$bt[2]['args'][0] . '): ' . $message;
}
trigger_error($message, E_USER_WARNING);
}
}
class Assert
{
public static function same($expected, $actual)
{
if ($expected === $actual) {
return;
}
echo 'EXPECTED: ' . var_export($expected, true) . "\n";
echo 'ACTUAL : ' . var_export($actual, true) . "\n";
throw new \Exception('Not same.');
}
}
$modes = ['r', 'r+', 'w', 'w+', 'a', 'a+', 'c', 'c+'];
$pathReal = __DIR__ . '/real-file.txt';
foreach ($modes as $mode) {
echo "Mode: $mode\n";
file_put_contents($pathReal, 'Hello');
$pathMock = FileMock::create('Hello');
$handleReal = fopen($pathReal, $mode);
$handleMock = fopen($pathMock, $mode);
Assert::same(ftell($handleReal), ftell($handleMock));
Assert::same(file_get_contents($pathReal), file_get_contents($pathMock));
Assert::same(fwrite($handleReal, 'World'), fwrite($handleMock, 'World'));
Assert::same(ftell($handleReal), ftell($handleMock));
Assert::same(file_get_contents($pathReal), file_get_contents($pathMock));
Assert::same(ftruncate($handleReal, 2), ftruncate($handleMock, 2));
Assert::same(ftell($handleReal), ftell($handleMock));
Assert::same(file_get_contents($pathReal), file_get_contents($pathMock));
Assert::same(fwrite($handleReal, 'World'), fwrite($handleMock, 'World'));
Assert::same(ftell($handleReal), ftell($handleMock));
Assert::same(file_get_contents($pathReal), file_get_contents($pathMock));
Assert::same(fseek($handleReal, 2), fseek($handleMock, 2));
Assert::same(fread($handleReal, 7), fread($handleMock, 7));
Assert::same(fclose($handleReal), fclose($handleMock));
unlink($pathReal);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment