-
-
Save milo/c13fe314c486451d2c545b923e73622f to your computer and use it in GitHub Desktop.
php7.3.0beta3 streamWrapper issue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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