Skip to content

Commit 583fdf1

Browse files
committed
[Filesystem] Add a cross-platform readlink/realpath methods for nested links
1 parent 47cb0c3 commit 583fdf1

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

src/Symfony/Component/Filesystem/Filesystem.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,72 @@ public function symlink($originDir, $targetDir, $copyOnWindows = false)
340340
}
341341
}
342342

343+
/**
344+
* Return the next direct target of a link.
345+
*
346+
* To recursively follow links until a final target
347+
* is reached, use @see Filesystem::realpath($path).
348+
*
349+
* @param string $path A link path.
350+
*
351+
* @return string Return the resolved link.
352+
*
353+
* @throws IOException When the link does not exist or is not readable.
354+
*/
355+
public function readlink($path)
356+
{
357+
if (!$this->exists($path)) {
358+
throw new IOException(sprintf('The link %s does not exist and cannot be read.', $path));
359+
}
360+
361+
if (!is_link($path)) {
362+
throw new IOException(sprintf('The path %s is not a link.', $path));
363+
}
364+
365+
// /link1 -> /link2 -> /file
366+
367+
// Windows: readlink(/link1) => /file
368+
// realpath(/link1) => /link2
369+
370+
// Unix: readlink(/link1) => /link2
371+
// realpath(/link1) => /file
372+
373+
if ('\\' === DIRECTORY_SEPARATOR) {
374+
return realpath($path);
375+
}
376+
377+
return readlink($path);
378+
}
379+
380+
/**
381+
* Return the final target of a link by recursively following links.
382+
*
383+
* To find only the direct next target of a link,
384+
* use @see Filesystem::readlink($path).
385+
*
386+
* @param string $path A link path.
387+
*
388+
* @return string Return the final target of the link.
389+
*
390+
* @throws IOException When the link does not exist or is not readable.
391+
*/
392+
public function realpath($path)
393+
{
394+
if (!$this->exists($path)) {
395+
throw new IOException(sprintf('The link %s does not exist and cannot be read.', $path));
396+
}
397+
398+
if (!is_link($path)) {
399+
throw new IOException(sprintf('The path %s is not a link.', $path));
400+
}
401+
402+
if ('\\' === DIRECTORY_SEPARATOR) {
403+
return readlink($path);
404+
}
405+
406+
return realpath($path);
407+
}
408+
343409
/**
344410
* Given an existing path, convert it to a path relative to a given starting path.
345411
*

src/Symfony/Component/Filesystem/Tests/FilesystemTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,80 @@ public function testSymlinkCreatesTargetDirectoryIfItDoesNotExist()
799799
$this->assertEquals($file, readlink($link2));
800800
}
801801

802+
public function testReadLink()
803+
{
804+
$this->markAsSkippedIfSymlinkIsMissing();
805+
806+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
807+
$link1 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'link';
808+
$link2 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'subdir'.DIRECTORY_SEPARATOR.'link';
809+
810+
touch($file);
811+
812+
$this->filesystem->symlink($file, $link1);
813+
$this->filesystem->symlink($link1, $link2);
814+
815+
$this->assertTrue(is_link($link1));
816+
$this->assertEquals($file, $this->filesystem->readlink($link1));
817+
$this->assertTrue(is_link($link2));
818+
$this->assertEquals($link1, $this->filesystem->readlink($link2));
819+
}
820+
821+
/**
822+
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
823+
*/
824+
public function testReadLinkNotLink()
825+
{
826+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
827+
touch($file);
828+
$this->filesystem->readlink($file);
829+
}
830+
831+
/**
832+
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
833+
*/
834+
public function testReadLinkFails()
835+
{
836+
$this->filesystem->readlink($this->workspace.DIRECTORY_SEPARATOR.'invalid');
837+
}
838+
839+
public function testRealPath()
840+
{
841+
$this->markAsSkippedIfSymlinkIsMissing();
842+
843+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
844+
$link1 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'link';
845+
$link2 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'subdir'.DIRECTORY_SEPARATOR.'link';
846+
847+
touch($file);
848+
849+
$this->filesystem->symlink($file, $link1);
850+
$this->filesystem->symlink($link1, $link2);
851+
852+
$this->assertTrue(is_link($link1));
853+
$this->assertEquals($file, $this->filesystem->realpath($link1));
854+
$this->assertTrue(is_link($link2));
855+
$this->assertEquals($file, $this->filesystem->realpath($link2));
856+
}
857+
858+
/**
859+
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
860+
*/
861+
public function testRealPathNotLink()
862+
{
863+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
864+
touch($file);
865+
$this->filesystem->realpath($file);
866+
}
867+
868+
/**
869+
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
870+
*/
871+
public function testRealPathFails()
872+
{
873+
$this->filesystem->realpath($this->workspace.DIRECTORY_SEPARATOR.'invalid');
874+
}
875+
802876
/**
803877
* @dataProvider providePathsForMakePathRelative
804878
*/

0 commit comments

Comments
 (0)