From 6212eadcb07349ed946fc8e9a7698f49d20084b7 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Mon, 11 Feb 2019 21:30:56 +0000 Subject: [PATCH] Only use junctions if they can be safely removed --- src/Composer/Downloader/PathDownloader.php | 30 ++++++++++++++++++++++ src/Composer/Util/Filesystem.php | 17 +++++++----- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index e7084bd97..a63a3d82d 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -82,6 +82,12 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $allowedStrategies = array(self::STRATEGY_MIRROR); } + // Check we can use junctions safely if we are on Windows + if (Platform::isWindows() && self::STRATEGY_SYMLINK === $currentStrategy && !$this->safeJunctions()) { + $currentStrategy = self::STRATEGY_MIRROR; + $allowedStrategies = array(self::STRATEGY_MIRROR); + } + $fileSystem = new Filesystem(); $this->filesystem->removeDirectory($path); @@ -172,4 +178,28 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter return $packageVersion['commit']; } } + + /** + * Returns true if junctions can be safely used on Windows + * + * A PHP bug makes junction detection fragile, leading to possible data loss + * when removing a package. See https://bugs.php.net/bug.php?id=77552 + * + * For safety we require a minimum version of Windows 7, so we can call the + * system rmdir which can detect junctions and not delete target content. + * + * @return bool + */ + private function safeJunctions() + { + // Bug fixed in 7.3.3 and 7.2.16 + if (PHP_VERSION_ID >= 70303 || (PHP_VERSION_ID >= 70216 && PHP_VERSION_ID < 70300)) { + return true; + } + + // Windows 7 is version 6.1 + return function_exists('proc_open') && + (PHP_WINDOWS_VERSION_MAJOR > 6 || + (PHP_WINDOWS_VERSION_MAJOR === 6 && PHP_WINDOWS_VERSION_MINOR >= 1)); + } } diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 8b0546f3a..c4e1b1b12 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -650,12 +650,17 @@ class Filesystem * * We test if the path is a directory and not an ordinary link, then check * that the mode value returned from lstat (which gives the status of the - * link itself) is not a directory. + * link itself) is not a directory, by replicating the POSIX S_ISDIR test. * - * This relies on the fact that PHP does not set this value because there is - * no universal file type flag for a junction or a mount point. However a - * bug in PHP can cause a random value to be returned and this could result - * in a junction not being detected: https://bugs.php.net/bug.php?id=77552 + * This logic works because PHP does not set the mode value for a junction, + * since there is no universal file type flag for it. Unfortunately an + * uninitialized variable in PHP prior to 7.2.16 and 7.3.3 may cause a + * random value to be returned. See https://bugs.php.net/bug.php?id=77552 + * + * If this random value passes the S_ISDIR test, then a junction will not be + * detected and a recursive delete operation could lead to loss of data in + * the target directory. Note that Windows rmdir can handle this situation + * and will only delete the junction (from Windows 7 onwards). * * @param string $junction Path to check. * @return bool @@ -673,7 +678,7 @@ class Filesystem clearstatcache(true, $junction); $stat = lstat($junction); - // S_IFDIR is 0x4000, S_IFMT is the 0xF000 bitmask + // S_ISDIR test (S_IFDIR is 0x4000, S_IFMT is 0xF000 bitmask) return $stat ? 0x4000 !== ($stat['mode'] & 0xF000) : false; }