From b71c67239d8a4e91dd158a06b24833ca70853013 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Tue, 2 Feb 2016 23:44:01 +0100 Subject: [PATCH] Made NTFS junction detection more reliable and added unit tests for the junction functions. --- src/Composer/Util/Filesystem.php | 38 +++++++++++++-------- tests/Composer/Test/Util/FilesystemTest.php | 29 ++++++++++++++++ 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index e8334d0a2..c3b888163 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -585,19 +585,23 @@ class Filesystem /** * Creates an NTFS junction. * - * @param string $originDir - * @param string $targetDir + * @param string $target + * @param string $junction */ - public function junction($originDir, $targetDir) + public function junction($target, $junction) { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - $cmd = sprintf('mklink /J %s %s', - ProcessExecutor::escape(str_replace('/', DIRECTORY_SEPARATOR, $targetDir)), - ProcessExecutor::escape(realpath($originDir))); - if ($this->getProcess()->execute($cmd) === 0) - return; + if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + throw new \LogicException(sprintf('Function %s is not available on non-Windows platform', __CLASS__)); + } + if (!is_dir($target)) { + throw new IOException(sprintf('Cannot junction to "%s" as it is not a directory.', $target), 0, null, $target); + } + $cmd = sprintf('mklink /J %s %s', + ProcessExecutor::escape(str_replace('/', DIRECTORY_SEPARATOR, $junction)), + ProcessExecutor::escape(realpath($target))); + if ($this->getProcess()->execute($cmd, $output) !== 0) { + throw new IOException(sprintf('Failed to create junction to "%s" at "%s".', $target, $junction), 0, null, $target); } - throw new IOException(sprintf('Failed to create junction from "%s" to "%s".', $originDir, $targetDir), 0, null, $targetDir); } /** @@ -611,9 +615,12 @@ class Filesystem if (!defined('PHP_WINDOWS_VERSION_BUILD')) { return false; } - $normalized = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $junction), DIRECTORY_SEPARATOR); - $real = rtrim(realpath($normalized), DIRECTORY_SEPARATOR); - return is_dir($normalized) && ($normalized !== $real); + if (!is_dir($junction) || is_link($junction)) { + return false; + } + // Junctions have no link stat but are otherwise indistinguishable from real directories + $stat = lstat($junction); + return ($stat['mode'] === 0); } /** @@ -628,7 +635,10 @@ class Filesystem return false; } $junction = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $junction), DIRECTORY_SEPARATOR); + if (!$this->isJunction($junction)) { + throw new IOException(sprintf('%s is not a junction and thus cannot be removed as one', $junction)); + } $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape($junction)); - return $this->getProcess()->execute($cmd) === 0; + return ($this->getProcess()->execute($cmd) === 0); } } diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index 969572036..550b0e3ce 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -266,4 +266,33 @@ class FilesystemTest extends TestCase $this->assertFalse(file_exists($symlinkedTrailingSlash)); $this->assertFalse(file_exists($symlinked)); } + + public function testJunctions() + { + @mkdir($this->workingDir . '/real/nesting/testing', 0777, true); + $fs = new Filesystem(); + + // Non-Windows systems do not support this and will return false on all tests, and an exception on creation + if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->assertFalse($fs->isJunction($this->workingDir)); + $this->assertFalse($fs->removeJunction($this->workingDir)); + $this->setExpectedException('LogicException', 'not available on non-Windows platform'); + } + + $target = $this->workingDir . '/real/../real/nesting'; + $junction = $this->workingDir . '/junction'; + + // Create and detect junction + $fs->junction($target, $junction); + $this->assertTrue($fs->isJunction($junction)); + $this->assertFalse($fs->isJunction($target)); + $this->assertTrue($fs->isJunction($target . '/../../junction')); + $this->assertFalse($fs->isJunction($junction . '/../real')); + $this->assertTrue($fs->isJunction($junction . '/../junction')); + + // Remove junction + $this->assertTrue(is_dir($junction)); + $this->assertTrue($fs->removeJunction($junction)); + $this->assertFalse(is_dir($junction)); + } }