diff --git a/src/Symfony/Component/Filesystem/CHANGELOG.md b/src/Symfony/Component/Filesystem/CHANGELOG.md index 9e303821e636..09ca3508c02a 100644 --- a/src/Symfony/Component/Filesystem/CHANGELOG.md +++ b/src/Symfony/Component/Filesystem/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.2.0 +----- + + * added `readlink()` as a platform independent method to read links + 3.0.0 ----- diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index f6debe9e6701..69548ba77503 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -383,6 +383,47 @@ private function linkException($origin, $target, $linkType) throw new IOException(sprintf('Failed to create %s link from "%s" to "%s".', $linkType, $origin, $target), 0, null, $target); } + /** + * Resolves links in paths. + * + * With $canonicalize = false (default) + * - if $path does not exist or is not a link, returns null + * - if $path is a link, returns the next direct target of the link without considering the existence of the target + * + * With $canonicalize = true + * - if $path does not exist, returns null + * - if $path exists, returns its absolute fully resolved final version + * + * @param string $path A filesystem path + * @param bool $canonicalize Whether or not to return a canonicalized path + * + * @return string|null + */ + public function readlink($path, $canonicalize = false) + { + if (!$canonicalize && !is_link($path)) { + return; + } + + if ($canonicalize) { + if (!$this->exists($path)) { + return; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + $path = readlink($path); + } + + return realpath($path); + } + + if ('\\' === DIRECTORY_SEPARATOR) { + return realpath($path); + } + + return readlink($path); + } + /** * Given an existing path, convert it to a path relative to a given starting path. * diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index b1d3d128b6fc..dc6338a8bbb6 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -957,6 +957,97 @@ public function testLinkWithSameTarget() $this->assertEquals(fileinode($file), fileinode($link)); } + public function testReadRelativeLink() + { + $this->markAsSkippedIfSymlinkIsMissing(); + + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Relative symbolic links are not supported on Windows'); + } + + $file = $this->workspace.'/file'; + $link1 = $this->workspace.'/dir/link'; + $link2 = $this->workspace.'/dir/link2'; + touch($file); + + $this->filesystem->symlink('../file', $link1); + $this->filesystem->symlink('link', $link2); + + $this->assertEquals($this->normalize('../file'), $this->filesystem->readlink($link1)); + $this->assertEquals('link', $this->filesystem->readlink($link2)); + $this->assertEquals($file, $this->filesystem->readlink($link1, true)); + $this->assertEquals($file, $this->filesystem->readlink($link2, true)); + $this->assertEquals($file, $this->filesystem->readlink($file, true)); + } + + public function testReadAbsoluteLink() + { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->normalize($this->workspace.'/file'); + $link1 = $this->normalize($this->workspace.'/dir/link'); + $link2 = $this->normalize($this->workspace.'/dir/link2'); + touch($file); + + $this->filesystem->symlink($file, $link1); + $this->filesystem->symlink($link1, $link2); + + $this->assertEquals($file, $this->filesystem->readlink($link1)); + $this->assertEquals($link1, $this->filesystem->readlink($link2)); + $this->assertEquals($file, $this->filesystem->readlink($link1, true)); + $this->assertEquals($file, $this->filesystem->readlink($link2, true)); + $this->assertEquals($file, $this->filesystem->readlink($file, true)); + } + + public function testReadBrokenLink() + { + $this->markAsSkippedIfSymlinkIsMissing(); + + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support creating "broken" symlinks'); + } + + $file = $this->workspace.'/file'; + $link = $this->workspace.'/link'; + + $this->filesystem->symlink($file, $link); + + $this->assertEquals($file, $this->filesystem->readlink($link)); + $this->assertNull($this->filesystem->readlink($link, true)); + + touch($file); + $this->assertEquals($file, $this->filesystem->readlink($link, true)); + } + + public function testReadLinkDefaultPathDoesNotExist() + { + $this->assertNull($this->filesystem->readlink($this->normalize($this->workspace.'/invalid'))); + } + + public function testReadLinkDefaultPathNotLink() + { + $file = $this->normalize($this->workspace.'/file'); + touch($file); + + $this->assertNull($this->filesystem->readlink($file)); + } + + public function testReadLinkCanonicalizePath() + { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->normalize($this->workspace.'/file'); + mkdir($this->normalize($this->workspace.'/dir')); + touch($file); + + $this->assertEquals($file, $this->filesystem->readlink($this->normalize($this->workspace.'/dir/../file'), true)); + } + + public function testReadLinkCanonicalizedPathDoesNotExist() + { + $this->assertNull($this->filesystem->readlink($this->normalize($this->workspace.'invalid'), true)); + } + /** * @dataProvider providePathsForMakePathRelative */ @@ -1321,4 +1412,16 @@ public function testCopyShouldKeepExecutionPermission() $this->assertFilePermissions(767, $targetFilePath); } + + /** + * Normalize the given path (transform each blackslash into a real directory separator). + * + * @param string $path + * + * @return string + */ + private function normalize($path) + { + return str_replace('/', DIRECTORY_SEPARATOR, $path); + } }