Skip to content

Commit

Permalink
feature #17498 [Filesystem] Add a cross-platform readlink method (tga…
Browse files Browse the repository at this point in the history
…lopin)

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
  • Loading branch information
fabpot committed Jul 30, 2016
2 parents 851a0a1 + c36507e commit f76d050
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 0 deletions.
5 changes: 5 additions & 0 deletions 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
-----

Expand Down
41 changes: 41 additions & 0 deletions src/Symfony/Component/Filesystem/Filesystem.php
Expand Up @@ -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.
*
Expand Down
103 changes: 103 additions & 0 deletions src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
}
}

0 comments on commit f76d050

Please sign in to comment.