feature #17498 [Filesystem] Add a cross-platform readlink method (tgalopin)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[Filesystem] Add a cross-platform readlink method

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

`readlink()` and `realpath()` have a completely different behavior under Windows and Unix:

- `realpath()` resolves recursively the children links of a link until a final target is found on Unix and resolves only the next link on Windows ;
- `readlink()` resolves recursively the children links of a link until a final target is found on Windows and resolves only the next link on Unix ;

I propose to solve this by implementing a helper method in the Filesystem component that would behave always the same way under all platforms.

Commits
-------

c36507e [Filesystem] Add a cross-platform readlink/realpath methods for nested links
This commit is contained in:
Fabien Potencier 2016-07-30 03:29:52 -04:00
commit f76d050dd4
3 changed files with 149 additions and 0 deletions

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
3.2.0
-----
* added `readlink()` as a platform independent method to read links
3.0.0
-----

View File

@ -383,6 +383,47 @@ class Filesystem
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.
*

View File

@ -957,6 +957,97 @@ class FilesystemTest extends FilesystemTestCase
$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 @@ class FilesystemTest extends FilesystemTestCase
$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);
}
}