From c6a774761dc75f992797eef35ae89e2c215a1dae Mon Sep 17 00:00:00 2001 From: Mark Challoner Date: Thu, 7 May 2015 19:37:53 +0100 Subject: [PATCH 1/5] [Filesystem] added tempnam() stream wrapper aware version of PHP's native tempnam() and fixed dumpFile to allow dumping to streams --- src/Symfony/Component/Filesystem/CHANGELOG.md | 5 + .../Component/Filesystem/Filesystem.php | 64 ++++- src/Symfony/Component/Filesystem/README.md | 4 + .../Filesystem/Tests/FilesystemTest.php | 134 +++++++++++ .../Tests/Fixtures/MockStream/MockStream.php | 226 ++++++++++++++++++ 5 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php diff --git a/src/Symfony/Component/Filesystem/CHANGELOG.md b/src/Symfony/Component/Filesystem/CHANGELOG.md index a4c0479f7d9a7..aee6e804b0d02 100644 --- a/src/Symfony/Component/Filesystem/CHANGELOG.md +++ b/src/Symfony/Component/Filesystem/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.8.0 +----- + + * added tempnam() a stream aware version of PHP's native tempnam() + 2.6.0 ----- diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 93a1f0de4c530..b10a264118d8e 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -452,6 +452,53 @@ public function isAbsolutePath($file) ); } + /** + * Creates a temporary file with support for custom stream wrappers. + * + * @param string $dir The directory where the temporary filename will be created. + * @param string $prefix The prefix of the generated temporary filename. + * Note: Windows uses only the first three characters of prefix. + * + * @return string The new temporary filename (with path), or false on failure. + */ + public function tempnam($dir, $prefix) + { + $limit = 10; + list($scheme, $hierarchy) = $this->getSchemeAndHierarchy($dir); + + // If no scheme or scheme is "file" create temp file in local filesystem + if (null === $scheme || 'file' === $scheme) { + $tmpFile = tempnam($hierarchy, $prefix); + + // If tempnam failed or no scheme return the filename otherwise prepend the scheme + return false === $tmpFile || null === $scheme ? $tmpFile : $scheme.'://'.$tmpFile; + } + + // Loop until we create a valid temp file or have reached $limit attempts + for ($i = 0; $i < $limit; $i++) { + + // Create a unique filename + $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true); + + // Use fopen instead of file_exists as some streams do not support stat + // Use mode 'x' to atomically check existence and create to avoid a TOCTOU vulnerability + $handle = @fopen($tmpFile, 'x'); + + // If unsuccessful restart the loop + if (false === $handle) { + continue; + } + + // Close the file if it was successfully opened + @fclose($handle); + + return $tmpFile; + + } + + return false; + } + /** * Atomically dumps content into a file. * @@ -472,7 +519,7 @@ public function dumpFile($filename, $content, $mode = 0666) throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir); } - $tmpFile = tempnam($dir, basename($filename)); + $tmpFile = $this->tempnam($dir, basename($filename)); if (false === @file_put_contents($tmpFile, $content)) { throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename); @@ -501,4 +548,19 @@ private function toIterator($files) return $files; } + + /** + * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> array(file, tmp)). + * + * @param string $filename The filename to be parsed. + * + * @return array The filename scheme and hierarchical part + */ + private function getSchemeAndHierarchy($filename) + { + $components = explode('://', $filename, 2); + + return count($components) >= 2 ? array($components[0], $components[1]) : array(null, $components[0]); + } + } diff --git a/src/Symfony/Component/Filesystem/README.md b/src/Symfony/Component/Filesystem/README.md index df09f93dce72a..466924f3b7d0e 100644 --- a/src/Symfony/Component/Filesystem/README.md +++ b/src/Symfony/Component/Filesystem/README.md @@ -30,11 +30,15 @@ $filesystem->rename($origin, $target); $filesystem->symlink($originDir, $targetDir, $copyOnWindows = false); +$filesystem->tempnam($dir, $prefix); + $filesystem->makePathRelative($endPath, $startPath); $filesystem->mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array()); $filesystem->isAbsolutePath($file); + +$filesystem->dumpFile($file, $content); ``` Resources diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index 45254c3c712e6..7eceacc273029 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Filesystem\Tests; +use Phar; /** * Test class for Filesystem. */ @@ -946,6 +947,116 @@ public function providePathsForIsAbsolutePath() ); } + public function testTempnam() + { + $dirname = $this->workspace; + + $filename = $this->filesystem->tempnam($dirname, 'foo'); + + $this->assertNotFalse($filename); + $this->assertFileExists($filename); + } + + public function testTempnamWithFileScheme() + { + $scheme = 'file://'; + $dirname = $scheme.$this->workspace; + + $filename = $this->filesystem->tempnam($dirname, 'foo'); + + $this->assertNotFalse($filename); + $this->assertStringStartsWith($scheme, $filename); + $this->assertFileExists($filename); + } + + public function testTempnamWithMockScheme() + { + // We avoid autoloading via ClassLoader as stream_wrapper_register creates the object + if (!@include __DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'MockStream'.DIRECTORY_SEPARATOR.'MockStream.php') { + $this->markTestSkipped('Unable to load mock:// stream.'); + } + + stream_wrapper_register('mock', 'MockStream\MockStream'); + + $scheme = 'mock://'; + $dirname = $scheme.$this->workspace; + + $filename = $this->filesystem->tempnam($dirname, 'foo'); + + $this->assertNotFalse($filename); + $this->assertStringStartsWith($scheme, $filename); + $this->assertFileExists($filename); + } + + public function testTempnamWithZlibSchemeFails() + { + $scheme = 'compress.zlib://'; + $dirname = $scheme.$this->workspace; + + $filename = $this->filesystem->tempnam($dirname, 'bar'); + + // The compress.zlib:// stream does not support mode x: creates the file, errors "failed to open stream: operation failed" and returns false + $this->assertFalse($filename); + } + + public function testTempnamWithPHPTempSchemeFails() + { + $scheme = 'php://temp'; + $dirname = $scheme; + + $filename = $this->filesystem->tempnam($dirname, 'bar'); + + $this->assertNotFalse($filename); + $this->assertStringStartsWith($scheme, $filename); + + // The php://temp stream deletes the file after close + $this->assertFileNotExists($filename); + } + + public function testTempnamWithPharSchemeFails() + { + // Skip test if Phar disabled phar.readonly must be 0 in php.ini + if (!Phar::canWrite()) { + $this->markTestSkipped('This test cannot run when phar.readonly is 1.'); + } + + $scheme = 'phar://'; + $dirname = $scheme.$this->workspace; + $pharname = 'foo.phar'; + + $p = new Phar($this->workspace.'/'.$pharname, 0, $pharname); + $filename = $this->filesystem->tempnam($dirname, $pharname.'/bar'); + + // The phar:// stream does not support mode x: fails to create file, errors "failed to open stream: phar error: "$filename" is not a file in phar "$pharname"" and returns false + $this->assertFalse($filename); + } + + public function testTempnamWithHTTPSchemeFails() + { + $scheme = 'http://'; + $dirname = $scheme.$this->workspace; + + $filename = $this->filesystem->tempnam($dirname, 'bar'); + + // The http:// scheme is read-only + $this->assertFalse($filename); + } + + public function testTempnamOnUnwritableFallsBackToSysTmp() + { + $scheme = 'file://'; + $dirname = $scheme.$this->workspace.DIRECTORY_SEPARATOR.'does_not_exist'; + + $filename = $this->filesystem->tempnam($dirname, 'bar'); + + $this->assertNotFalse($filename); + $this->assertStringStartsWith(rtrim($scheme.sys_get_temp_dir(), DIRECTORY_SEPARATOR), $filename); + $this->assertFileExists($filename); + + // Tear down + unlink($filename); + } + public function testDumpFile() { $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; @@ -1000,6 +1111,29 @@ public function testDumpFileOverwritesAnExistingFile() $this->assertSame('bar', file_get_contents($filename)); } + public function testDumpFileWithFileScheme() + { + $scheme = 'file://'; + $filename = $scheme.$this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; + + $this->filesystem->dumpFile($filename, 'bar', null); + + $this->assertFileExists($filename); + $this->assertSame('bar', file_get_contents($filename)); + } + + public function testDumpFileWithZlibScheme() + { + $scheme = 'compress.zlib://'; + $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; + + $this->filesystem->dumpFile($filename, 'bar', null); + + // Zlib stat uses file:// wrapper so remove scheme + $this->assertFileExists(str_replace($scheme, '', $filename)); + $this->assertSame('bar', file_get_contents($filename)); + } + public function testCopyShouldKeepExecutionPermission() { $this->markAsSkippedIfChmodIsMissing(); diff --git a/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php b/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php new file mode 100644 index 0000000000000..ac1a840d23540 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php @@ -0,0 +1,226 @@ +. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * This class is based on VariableStream from the PHP Manual, which is licenced + * under Creative Commons Attribution 3.0 Licence copyright (c) the PHP + * Documentation Group + * + * @url http://php.net/manual/en/stream.streamwrapper.example-1.php + * @url http://php.net/license/ + * @url http://creativecommons.org/licenses/by/3.0/legalcode + */ +namespace MockStream; + +/** + * Mock stream class to be used with stream_wrapper_register. + * + * stream_wrapper_register('mock', 'MockStream\MockStream') + */ +class MockStream { + private $str_overloaded; + private $content; + private $position; + private $atime; + private $mtime; + private $ctime; + private $path; + + /** + * Opens file or URL. + * + * @param string $path Specifies the URL that was passed to the original function + * @param string $mode The mode used to open the file, as detailed for fopen() + * @param int $options Holds additional flags set by the streams API + * @param string $opened_path If the path is opened successfully, and STREAM_USE_PATH is set in options, opened_path should be set to the full path of the file/resource that was actually opened + * + * @return bool + */ + public function stream_open($path, $mode, $options, &$opened_path) { + // Is mbstring.func_overload applied to string functions (bit 2 set) + $this->str_overloaded = (bool) (ini_get('mbstring.func_overload') & (1 << 2)); + $this->path = $path; + $this->content = ''; + $this->position = 0; + $this->atime = 0; + $this->mtime = 0; + + return true; + } + + /** + * Read from stream. + * + * @param int $count How many bytes of data from the current position should be returned + * + * @return string The data + */ + public function stream_read($count) { + $ret = $this->substr($this->varname, $this->position, $count); + $this->position += $this->strlen($ret); + $this->atime = time(); + + return $ret; + } + + /** + * Write to stream. + * + * @param string $data Data to write to the stream + * + * @return int Number of bytes that were successfully stored, or 0 if none could be stored + */ + public function stream_write($data) { + $left = $this->substr($this->content, 0, $this->position); + $right = $this->substr($this->content, $this->position + $this->strlen($data)); + $this->content = $left.$data.$right; + $this->position += $this->strlen($data); + $this->mtime = time(); + $this->ctime = time(); + + return $this->strlen($data); + } + + /** + * Retrieve the current position of a stream. + * + * @return int The current position of the stream + */ + public function stream_tell() { + return $this->position; + } + + /** + * Tests for end-of-file on a file pointer. + * + * @return bool Return true if the read/write position is at the end of the stream and if no more data is available to be read, or false otherwise + */ + public function stream_eof() { + return $this->position >= $this->strlen($this->content); + } + + /** + * Seeks to specific location in a stream. + * + * @param string $offset The stream offset to seek to + * @param int $whence Set position based on value + * + * @return bool Return true if the position was updated, false otherwise + */ + public function stream_seek($offset, $whence) { + switch ($whence) { + case SEEK_SET: + if ($offset < $this->strlen($this->content) && 0 <= $offset) { + $this->position = $offset; + + return true; + } + break; + + case SEEK_CUR: + if (0 <= $offset) { + $this->position += $offset; + + return true; + } + break; + + case SEEK_END: + if (0 <= $this->strlen($this->content) + $offset) { + $this->position = $this->strlen($this->content) + $offset; + + return true; + } + break; + } + + return false; + } + + /** + * Change stream options, only touch is supported. + * + * @param string $path The file path or URL to set metadata + * @param array $option + * @param array $value Additional arguments for the option + * + * @return bool Return true on success or fale on failure or if option is not implemented + */ + public function stream_metadata($path, $option, $value) { + if (STREAM_META_TOUCH === $option) { + $now = array_key_exists(0, $value) ? $value[0] : time(); + $this->atime = array_key_exists(1, $value) ? $value[1] : $now; + $this->mtime = $now; + $this->ctime = $now; + + return true; + } + + return false; + } + + /** + * Retrieve information about a stream. + * + * @return array Stream stats + */ + public function stream_stat() { + return array( + 'dev' => 0, + 'ino' => 0, + 'mode' => 33188, // 100644 + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $this->strlen($this->content), + 'atime' => $this->atime, + 'mtime' => $this->mtime, + 'ctime' => $this->ctime, + 'blksize' => 4096, + 'blocks' => 8, + ); + } + + /** + * Retrieve information about a url, added as called by PHP's builtin functions. + * + * @param string $path The file path or URL to stat + * @param array $flags Holds additional flags set by the streams API + * + * @return array File stats + */ + public function url_stat($path, $flags) { + return $this->stream_stat(); + } + + /** + * Returns the number of bytes of the given string even when strlen is overloaded to mb_strlen. + * + * @param string $string The string being measured for bytes + * + * @return int The number of bytes in the string on success, and 0 if the string is empty + */ + protected function strlen($string) { + return function_exists('mb_strlen') && $this->str_overloaded ? mb_strlen($string, '8bit') : strlen($string); + } + + /** + * Returns the portion of string specified by the start and length parameters even when substr is overloaded to mb_substr. + * + * @param string $string The input string which must be one character or longer + * @param start $int Starting position in bytes + * @param length $int Length in bytes which if omitted or NULL is passed, extracts all bytes to the end of the string + * + * @return string + */ + protected function substr($string, $start, $length = null) { + return function_exists('mb_substr') && $this->str_overloaded ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); + } + +} From 247266cdef84e6352173f564de06151027550c8c Mon Sep 17 00:00:00 2001 From: Pierre du Plessis Date: Wed, 7 Oct 2015 00:13:52 +0200 Subject: [PATCH 2/5] Update coding standard for MockStream --- .../Component/Filesystem/Filesystem.php | 16 +++- src/Symfony/Component/Filesystem/README.md | 4 - .../Tests/Fixtures/MockStream/MockStream.php | 91 ++++++++++--------- 3 files changed, 62 insertions(+), 49 deletions(-) diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index b10a264118d8e..f9e462a6bd770 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -468,14 +468,24 @@ public function tempnam($dir, $prefix) // If no scheme or scheme is "file" create temp file in local filesystem if (null === $scheme || 'file' === $scheme) { + $tmpFile = tempnam($hierarchy, $prefix); // If tempnam failed or no scheme return the filename otherwise prepend the scheme - return false === $tmpFile || null === $scheme ? $tmpFile : $scheme.'://'.$tmpFile; + + if (null !== $scheme) { + return $scheme.'://'.$tmpFile; + } + + if (false !== $tmpFile) { + return $tmpFile; + } + + throw new IOException('A temporary file could not be created'); } // Loop until we create a valid temp file or have reached $limit attempts - for ($i = 0; $i < $limit; $i++) { + for ($i = 0; $i < $limit; ++$i) { // Create a unique filename $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true); @@ -560,7 +570,7 @@ private function getSchemeAndHierarchy($filename) { $components = explode('://', $filename, 2); - return count($components) >= 2 ? array($components[0], $components[1]) : array(null, $components[0]); + return count($components) === 2 ? array($components[0], $components[1]) : array(null, $components[0]); } } diff --git a/src/Symfony/Component/Filesystem/README.md b/src/Symfony/Component/Filesystem/README.md index 466924f3b7d0e..df09f93dce72a 100644 --- a/src/Symfony/Component/Filesystem/README.md +++ b/src/Symfony/Component/Filesystem/README.md @@ -30,15 +30,11 @@ $filesystem->rename($origin, $target); $filesystem->symlink($originDir, $targetDir, $copyOnWindows = false); -$filesystem->tempnam($dir, $prefix); - $filesystem->makePathRelative($endPath, $startPath); $filesystem->mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array()); $filesystem->isAbsolutePath($file); - -$filesystem->dumpFile($file, $content); ``` Resources diff --git a/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php b/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php index ac1a840d23540..7dabbfd80522a 100644 --- a/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php +++ b/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php @@ -1,29 +1,23 @@ . + * + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. - * - * This class is based on VariableStream from the PHP Manual, which is licenced - * under Creative Commons Attribution 3.0 Licence copyright (c) the PHP - * Documentation Group - * - * @url http://php.net/manual/en/stream.streamwrapper.example-1.php - * @url http://php.net/license/ - * @url http://creativecommons.org/licenses/by/3.0/legalcode */ + namespace MockStream; /** * Mock stream class to be used with stream_wrapper_register. - * - * stream_wrapper_register('mock', 'MockStream\MockStream') + * stream_wrapper_register('mock', 'MockStream\MockStream'). */ -class MockStream { - private $str_overloaded; +class MockStream +{ + private $strOverloaded; private $content; private $position; private $atime; @@ -37,13 +31,16 @@ class MockStream { * @param string $path Specifies the URL that was passed to the original function * @param string $mode The mode used to open the file, as detailed for fopen() * @param int $options Holds additional flags set by the streams API - * @param string $opened_path If the path is opened successfully, and STREAM_USE_PATH is set in options, opened_path should be set to the full path of the file/resource that was actually opened + * @param string $opened_path If the path is opened successfully, and STREAM_USE_PATH is set in options, + * opened_path should be set to the full path of the file/resource that was actually + * opened * * @return bool */ - public function stream_open($path, $mode, $options, &$opened_path) { + public function stream_open($path, $mode, $options, &$opened_path) + { // Is mbstring.func_overload applied to string functions (bit 2 set) - $this->str_overloaded = (bool) (ini_get('mbstring.func_overload') & (1 << 2)); + $this->strOverloaded = (bool) (ini_get('mbstring.func_overload') & (1 << 2)); $this->path = $path; $this->content = ''; $this->position = 0; @@ -60,8 +57,9 @@ public function stream_open($path, $mode, $options, &$opened_path) { * * @return string The data */ - public function stream_read($count) { - $ret = $this->substr($this->varname, $this->position, $count); + public function stream_read($count) + { + $ret = $this->substr($this->content, $this->position, $count); $this->position += $this->strlen($ret); $this->atime = time(); @@ -75,7 +73,8 @@ public function stream_read($count) { * * @return int Number of bytes that were successfully stored, or 0 if none could be stored */ - public function stream_write($data) { + public function stream_write($data) + { $left = $this->substr($this->content, 0, $this->position); $right = $this->substr($this->content, $this->position + $this->strlen($data)); $this->content = $left.$data.$right; @@ -91,50 +90,54 @@ public function stream_write($data) { * * @return int The current position of the stream */ - public function stream_tell() { + public function stream_tell() + { return $this->position; } /** * Tests for end-of-file on a file pointer. * - * @return bool Return true if the read/write position is at the end of the stream and if no more data is available to be read, or false otherwise + * @return bool Return true if the read/write position is at the end of the stream and if no more data is available + * to be read, or false otherwise */ - public function stream_eof() { + public function stream_eof() + { return $this->position >= $this->strlen($this->content); } /** * Seeks to specific location in a stream. * - * @param string $offset The stream offset to seek to - * @param int $whence Set position based on value + * @param int $offset The stream offset to seek to + * @param int $whence Set position based on value * * @return bool Return true if the position was updated, false otherwise */ - public function stream_seek($offset, $whence) { + public function stream_seek($offset, $whence) + { switch ($whence) { case SEEK_SET: if ($offset < $this->strlen($this->content) && 0 <= $offset) { - $this->position = $offset; + $this->position = $offset; - return true; + return true; } break; case SEEK_CUR: if (0 <= $offset) { - $this->position += $offset; + $this->position += $offset; - return true; + return true; } break; case SEEK_END: if (0 <= $this->strlen($this->content) + $offset) { - $this->position = $this->strlen($this->content) + $offset; + $this->position = $this->strlen($this->content) + $offset; - return true; + return true; } break; } @@ -151,7 +154,8 @@ public function stream_seek($offset, $whence) { * * @return bool Return true on success or fale on failure or if option is not implemented */ - public function stream_metadata($path, $option, $value) { + public function stream_metadata($path, $option, $value) + { if (STREAM_META_TOUCH === $option) { $now = array_key_exists(0, $value) ? $value[0] : time(); $this->atime = array_key_exists(1, $value) ? $value[1] : $now; @@ -169,7 +173,8 @@ public function stream_metadata($path, $option, $value) { * * @return array Stream stats */ - public function stream_stat() { + public function stream_stat() + { return array( 'dev' => 0, 'ino' => 0, @@ -195,7 +200,8 @@ public function stream_stat() { * * @return array File stats */ - public function url_stat($path, $flags) { + public function url_stat($path, $flags) + { return $this->stream_stat(); } @@ -206,21 +212,22 @@ public function url_stat($path, $flags) { * * @return int The number of bytes in the string on success, and 0 if the string is empty */ - protected function strlen($string) { - return function_exists('mb_strlen') && $this->str_overloaded ? mb_strlen($string, '8bit') : strlen($string); + protected function strlen($string) + { + return function_exists('mb_strlen') && $this->strOverloaded ? mb_strlen($string, '8bit') : strlen($string); } /** * Returns the portion of string specified by the start and length parameters even when substr is overloaded to mb_substr. * * @param string $string The input string which must be one character or longer - * @param start $int Starting position in bytes - * @param length $int Length in bytes which if omitted or NULL is passed, extracts all bytes to the end of the string + * @param int $start Starting position in bytes + * @param int $length Length in bytes which if omitted or NULL is passed, extracts all bytes to the end of the string * * @return string */ - protected function substr($string, $start, $length = null) { - return function_exists('mb_substr') && $this->str_overloaded ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); + protected function substr($string, $start, $length = null) + { + return function_exists('mb_substr') && $this->strOverloaded ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); } - } From 61a3afd829e3b76e39a18dca7635921a9148c51b Mon Sep 17 00:00:00 2001 From: Pierre du Plessis Date: Thu, 8 Oct 2015 12:58:45 +0200 Subject: [PATCH 3/5] Removed unused logic in MockStream --- .../Component/Filesystem/Filesystem.php | 26 +-- .../Filesystem/Tests/FilesystemTest.php | 39 ++-- .../Tests/Fixtures/MockStream/MockStream.php | 195 +----------------- 3 files changed, 30 insertions(+), 230 deletions(-) diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index f9e462a6bd770..194771ea083d3 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -459,34 +459,30 @@ public function isAbsolutePath($file) * @param string $prefix The prefix of the generated temporary filename. * Note: Windows uses only the first three characters of prefix. * - * @return string The new temporary filename (with path), or false on failure. + * @return string The new temporary filename (with path), or throw an exception on failure. */ public function tempnam($dir, $prefix) { - $limit = 10; list($scheme, $hierarchy) = $this->getSchemeAndHierarchy($dir); // If no scheme or scheme is "file" create temp file in local filesystem if (null === $scheme || 'file' === $scheme) { - $tmpFile = tempnam($hierarchy, $prefix); // If tempnam failed or no scheme return the filename otherwise prepend the scheme - - if (null !== $scheme) { - return $scheme.'://'.$tmpFile; - } - if (false !== $tmpFile) { + if (null !== $scheme) { + return $scheme.'://'.$tmpFile; + } + return $tmpFile; } - throw new IOException('A temporary file could not be created'); + throw new IOException('A temporary file could not be created.'); } - // Loop until we create a valid temp file or have reached $limit attempts - for ($i = 0; $i < $limit; ++$i) { - + // Loop until we create a valid temp file or have reached 10 attempts + for ($i = 0; $i < 10; ++$i) { // Create a unique filename $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true); @@ -503,10 +499,9 @@ public function tempnam($dir, $prefix) @fclose($handle); return $tmpFile; - } - return false; + throw new IOException('A temporary file could not be created.'); } /** @@ -570,7 +565,6 @@ private function getSchemeAndHierarchy($filename) { $components = explode('://', $filename, 2); - return count($components) === 2 ? array($components[0], $components[1]) : array(null, $components[0]); + return 2 === count($components) ? array($components[0], $components[1]) : array(null, $components[0]); } - } diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index 7eceacc273029..b9ad6c26f5b64 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Filesystem\Tests; -use Phar; /** * Test class for Filesystem. */ @@ -953,7 +952,6 @@ public function testTempnam() $filename = $this->filesystem->tempnam($dirname, 'foo'); - $this->assertNotFalse($filename); $this->assertFileExists($filename); } @@ -964,39 +962,34 @@ public function testTempnamWithFileScheme() $filename = $this->filesystem->tempnam($dirname, 'foo'); - $this->assertNotFalse($filename); $this->assertStringStartsWith($scheme, $filename); $this->assertFileExists($filename); } public function testTempnamWithMockScheme() { - // We avoid autoloading via ClassLoader as stream_wrapper_register creates the object - if (!@include __DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'MockStream'.DIRECTORY_SEPARATOR.'MockStream.php') { - $this->markTestSkipped('Unable to load mock:// stream.'); - } - - stream_wrapper_register('mock', 'MockStream\MockStream'); + stream_wrapper_register('mock', 'Symfony\Component\Filesystem\Tests\Fixtures\MockStream\MockStream'); $scheme = 'mock://'; $dirname = $scheme.$this->workspace; $filename = $this->filesystem->tempnam($dirname, 'foo'); - $this->assertNotFalse($filename); $this->assertStringStartsWith($scheme, $filename); $this->assertFileExists($filename); } + /** + * @expectedException \Symfony\Component\Filesystem\Exception\IOException + */ public function testTempnamWithZlibSchemeFails() { $scheme = 'compress.zlib://'; $dirname = $scheme.$this->workspace; - $filename = $this->filesystem->tempnam($dirname, 'bar'); - // The compress.zlib:// stream does not support mode x: creates the file, errors "failed to open stream: operation failed" and returns false - $this->assertFalse($filename); + $this->filesystem->tempnam($dirname, 'bar'); + } public function testTempnamWithPHPTempSchemeFails() @@ -1006,17 +999,19 @@ public function testTempnamWithPHPTempSchemeFails() $filename = $this->filesystem->tempnam($dirname, 'bar'); - $this->assertNotFalse($filename); $this->assertStringStartsWith($scheme, $filename); // The php://temp stream deletes the file after close $this->assertFileNotExists($filename); } + /** + * @expectedException \Symfony\Component\Filesystem\Exception\IOException + */ public function testTempnamWithPharSchemeFails() { // Skip test if Phar disabled phar.readonly must be 0 in php.ini - if (!Phar::canWrite()) { + if (!\Phar::canWrite()) { $this->markTestSkipped('This test cannot run when phar.readonly is 1.'); } @@ -1024,22 +1019,21 @@ public function testTempnamWithPharSchemeFails() $dirname = $scheme.$this->workspace; $pharname = 'foo.phar'; - $p = new Phar($this->workspace.'/'.$pharname, 0, $pharname); - $filename = $this->filesystem->tempnam($dirname, $pharname.'/bar'); - + new \Phar($this->workspace.'/'.$pharname, 0, $pharname); // The phar:// stream does not support mode x: fails to create file, errors "failed to open stream: phar error: "$filename" is not a file in phar "$pharname"" and returns false - $this->assertFalse($filename); + $this->filesystem->tempnam($dirname, $pharname.'/bar'); } + /** + * @expectedException \Symfony\Component\Filesystem\Exception\IOException + */ public function testTempnamWithHTTPSchemeFails() { $scheme = 'http://'; $dirname = $scheme.$this->workspace; - $filename = $this->filesystem->tempnam($dirname, 'bar'); - // The http:// scheme is read-only - $this->assertFalse($filename); + $this->filesystem->tempnam($dirname, 'bar'); } public function testTempnamOnUnwritableFallsBackToSysTmp() @@ -1049,7 +1043,6 @@ public function testTempnamOnUnwritableFallsBackToSysTmp() $filename = $this->filesystem->tempnam($dirname, 'bar'); - $this->assertNotFalse($filename); $this->assertStringStartsWith(rtrim($scheme.sys_get_temp_dir(), DIRECTORY_SEPARATOR), $filename); $this->assertFileExists($filename); diff --git a/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php b/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php index 7dabbfd80522a..f14420fb60a1a 100644 --- a/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php +++ b/src/Symfony/Component/Filesystem/Tests/Fixtures/MockStream/MockStream.php @@ -9,22 +9,14 @@ * file that was distributed with this source code. */ -namespace MockStream; +namespace Symfony\Component\Filesystem\Tests\Fixtures\MockStream; /** * Mock stream class to be used with stream_wrapper_register. - * stream_wrapper_register('mock', 'MockStream\MockStream'). + * stream_wrapper_register('mock', 'Symfony\Component\Filesystem\Tests\Fixtures\MockStream\MockStream'). */ class MockStream { - private $strOverloaded; - private $content; - private $position; - private $atime; - private $mtime; - private $ctime; - private $path; - /** * Opens file or URL. * @@ -32,169 +24,16 @@ class MockStream * @param string $mode The mode used to open the file, as detailed for fopen() * @param int $options Holds additional flags set by the streams API * @param string $opened_path If the path is opened successfully, and STREAM_USE_PATH is set in options, - * opened_path should be set to the full path of the file/resource that was actually - * opened + * opened_path should be set to the full path of the file/resource that was actually opened * * @return bool */ public function stream_open($path, $mode, $options, &$opened_path) { - // Is mbstring.func_overload applied to string functions (bit 2 set) - $this->strOverloaded = (bool) (ini_get('mbstring.func_overload') & (1 << 2)); - $this->path = $path; - $this->content = ''; - $this->position = 0; - $this->atime = 0; - $this->mtime = 0; - return true; } /** - * Read from stream. - * - * @param int $count How many bytes of data from the current position should be returned - * - * @return string The data - */ - public function stream_read($count) - { - $ret = $this->substr($this->content, $this->position, $count); - $this->position += $this->strlen($ret); - $this->atime = time(); - - return $ret; - } - - /** - * Write to stream. - * - * @param string $data Data to write to the stream - * - * @return int Number of bytes that were successfully stored, or 0 if none could be stored - */ - public function stream_write($data) - { - $left = $this->substr($this->content, 0, $this->position); - $right = $this->substr($this->content, $this->position + $this->strlen($data)); - $this->content = $left.$data.$right; - $this->position += $this->strlen($data); - $this->mtime = time(); - $this->ctime = time(); - - return $this->strlen($data); - } - - /** - * Retrieve the current position of a stream. - * - * @return int The current position of the stream - */ - public function stream_tell() - { - return $this->position; - } - - /** - * Tests for end-of-file on a file pointer. - * - * @return bool Return true if the read/write position is at the end of the stream and if no more data is available - * to be read, or false otherwise - */ - public function stream_eof() - { - return $this->position >= $this->strlen($this->content); - } - - /** - * Seeks to specific location in a stream. - * - * @param int $offset The stream offset to seek to - * @param int $whence Set position based on value - * - * @return bool Return true if the position was updated, false otherwise - */ - public function stream_seek($offset, $whence) - { - switch ($whence) { - case SEEK_SET: - if ($offset < $this->strlen($this->content) && 0 <= $offset) { - $this->position = $offset; - - return true; - } - break; - - case SEEK_CUR: - if (0 <= $offset) { - $this->position += $offset; - - return true; - } - break; - - case SEEK_END: - if (0 <= $this->strlen($this->content) + $offset) { - $this->position = $this->strlen($this->content) + $offset; - - return true; - } - break; - } - - return false; - } - - /** - * Change stream options, only touch is supported. - * - * @param string $path The file path or URL to set metadata - * @param array $option - * @param array $value Additional arguments for the option - * - * @return bool Return true on success or fale on failure or if option is not implemented - */ - public function stream_metadata($path, $option, $value) - { - if (STREAM_META_TOUCH === $option) { - $now = array_key_exists(0, $value) ? $value[0] : time(); - $this->atime = array_key_exists(1, $value) ? $value[1] : $now; - $this->mtime = $now; - $this->ctime = $now; - - return true; - } - - return false; - } - - /** - * Retrieve information about a stream. - * - * @return array Stream stats - */ - public function stream_stat() - { - return array( - 'dev' => 0, - 'ino' => 0, - 'mode' => 33188, // 100644 - 'nlink' => 1, - 'uid' => 0, - 'gid' => 0, - 'rdev' => 0, - 'size' => $this->strlen($this->content), - 'atime' => $this->atime, - 'mtime' => $this->mtime, - 'ctime' => $this->ctime, - 'blksize' => 4096, - 'blocks' => 8, - ); - } - - /** - * Retrieve information about a url, added as called by PHP's builtin functions. - * * @param string $path The file path or URL to stat * @param array $flags Holds additional flags set by the streams API * @@ -202,32 +41,6 @@ public function stream_stat() */ public function url_stat($path, $flags) { - return $this->stream_stat(); - } - - /** - * Returns the number of bytes of the given string even when strlen is overloaded to mb_strlen. - * - * @param string $string The string being measured for bytes - * - * @return int The number of bytes in the string on success, and 0 if the string is empty - */ - protected function strlen($string) - { - return function_exists('mb_strlen') && $this->strOverloaded ? mb_strlen($string, '8bit') : strlen($string); - } - - /** - * Returns the portion of string specified by the start and length parameters even when substr is overloaded to mb_substr. - * - * @param string $string The input string which must be one character or longer - * @param int $start Starting position in bytes - * @param int $length Length in bytes which if omitted or NULL is passed, extracts all bytes to the end of the string - * - * @return string - */ - protected function substr($string, $start, $length = null) - { - return function_exists('mb_substr') && $this->strOverloaded ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); + return array(); } } From a17aa5e091585ef0599e941a1cbc0d786833d85b Mon Sep 17 00:00:00 2001 From: Pierre du Plessis Date: Mon, 12 Oct 2015 17:04:38 +0200 Subject: [PATCH 4/5] Fixed failing test for HHVM --- src/Symfony/Component/Filesystem/Filesystem.php | 2 +- src/Symfony/Component/Filesystem/Tests/FilesystemTest.php | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 194771ea083d3..ff2a66746cd7b 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -565,6 +565,6 @@ private function getSchemeAndHierarchy($filename) { $components = explode('://', $filename, 2); - return 2 === count($components) ? array($components[0], $components[1]) : array(null, $components[0]); + return 2 === count($components) ? array($components[0], $components[1]) : array(null, $components[0]); } } diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index b9ad6c26f5b64..ea38e17df567e 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -1047,7 +1047,7 @@ public function testTempnamOnUnwritableFallsBackToSysTmp() $this->assertFileExists($filename); // Tear down - unlink($filename); + @unlink($filename); } public function testDumpFile() @@ -1106,6 +1106,10 @@ public function testDumpFileOverwritesAnExistingFile() public function testDumpFileWithFileScheme() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM does not handle the file:// scheme correctly'); + } + $scheme = 'file://'; $filename = $scheme.$this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; From 5ca7dee2fd024681da037b9a3ec3f3c8c2c34f8c Mon Sep 17 00:00:00 2001 From: Pierre du Plessis Date: Mon, 19 Oct 2015 10:45:30 +0200 Subject: [PATCH 5/5] Fix mode --- src/Symfony/Component/Filesystem/Filesystem.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index ff2a66746cd7b..7b19706e6cedd 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -487,8 +487,8 @@ public function tempnam($dir, $prefix) $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true); // Use fopen instead of file_exists as some streams do not support stat - // Use mode 'x' to atomically check existence and create to avoid a TOCTOU vulnerability - $handle = @fopen($tmpFile, 'x'); + // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability + $handle = @fopen($tmpFile, 'x+'); // If unsuccessful restart the loop if (false === $handle) {