Skip to content

Commit

Permalink
[Filesystem] added tempnam() stream wrapper aware version of PHP's na…
Browse files Browse the repository at this point in the history
…tive tempnam() and fixed dumpFile to allow dumping to streams
  • Loading branch information
markchalloner authored and pierredup committed Nov 9, 2015
1 parent 0bb46c1 commit c6a7747
Show file tree
Hide file tree
Showing 5 changed files with 432 additions and 1 deletion.
5 changes: 5 additions & 0 deletions 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
-----

Expand Down
64 changes: 63 additions & 1 deletion src/Symfony/Component/Filesystem/Filesystem.php
Expand Up @@ -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.
*
Expand All @@ -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);
Expand Down Expand Up @@ -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]);
}

}
4 changes: 4 additions & 0 deletions src/Symfony/Component/Filesystem/README.md
Expand Up @@ -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
Expand Down
134 changes: 134 additions & 0 deletions src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Filesystem\Tests;

use Phar;
/**
* Test class for Filesystem.
*/
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit c6a7747

Please sign in to comment.