getCwd(); $relativePath = $this->findShortestPath($link, $target); chdir(dirname($link)); $result = @symlink($relativePath, $link); chdir($cwd); return $result; } /** * Return true if that directory is a symlink. */ public function isSymlinkedDirectory(string $directory): bool { if (!is_dir($directory)) { return false; } $resolved = $this->resolveSymlinkedDirectorySymlink($directory); return is_link($resolved); } /** * Return true if that file is a symlink. */ public function isSymlinkedFile(string $file): bool { if (!is_file($file)) { return false; } return is_link($file); } /** * Creates an NTFS junction. */ public function junction(string $target, string $junction): void { if (!Environment::isWindows()) { throw new \LogicException(sprintf('Function %s is not available on non-Windows platform', __CLASS__), 1765283168); } if (!is_dir($target)) { throw new IOException(sprintf('Cannot junction to "%s" as it is not a directory.', $target), 1765283131, null, $target); } // Removing any previously junction to ensure clean execution. if (!is_dir($junction) || $this->isJunction($junction)) { @rmdir($junction); } $commandLine = [ 'mklink', '/J', ]; $commandLine[] = str_replace('/', DIRECTORY_SEPARATOR, $junction); $commandLine[] = realpath($target); CommandUtility::exec($commandLine); if (CommandUtility::exec($commandLine) === false) { throw new IOException(sprintf('Failed to create junction to "%s" at "%s".', $target, $junction), 1763664408, null, $target); } clearstatcache(true, $junction); } /** * Returns whether the target directory is a Windows NTFS Junction. * * 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, by replicating the POSIX S_ISDIR test. * * @param string $junction Path to check. */ public function isJunction(string $junction): bool { if (!Environment::isWindows()) { return false; } // Important to clear all caches first clearstatcache(true, $junction); if (!is_dir($junction) || is_link($junction)) { return false; } $stat = lstat($junction); // S_ISDIR test (S_IFDIR is 0x4000, S_IFMT is 0xF000 bitmask) return is_array($stat) && ($stat['mode'] & 0xF000) !== 0x4000; } /** * Resolve pathname to symbolic link of a directory * * @param string $pathname Directory path to resolve */ private function resolveSymlinkedDirectorySymlink(string $pathname): string { if (!is_dir($pathname)) { return $pathname; } $resolved = rtrim($pathname, '/'); if ($resolved === '') { return $pathname; } return $resolved; } /** * getcwd() equivalent which always returns a string * * @throws \RuntimeException */ private function getCwd(): string { $cwd = getcwd(); // fallback to realpath('') just in case this works but odds are it would break as well if we are in a case where getcwd fails if ($cwd === false) { $cwd = realpath(''); } // crappy state, assume '' and hopefully relative paths allow things to continue if ($cwd === false) { throw new \RuntimeException('Could not determine the current working directory', 1765283181); } return $cwd; } }