8000 [Filesystem] Add a cross-platform readlink method · symfony/symfony@0b94a11 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0b94a11

Browse files
committed
[Filesystem] Add a cross-platform readlink method
1 parent bb2727a commit 0b94a11

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

src/Symfony/Component/Filesystem/Filesystem.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,76 @@ public function symlink($originDir, $targetDir, $copyOnWindows = false)
337337
}
338338
}
339339

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

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,78 @@ 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+
public function testReadLinkNotLink()
822+
{
823+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
824+
825+
touch($file);
826+
827+
$this->assertEquals($file, $this->filesystem->readlink($file));
828+
}
829+
830+
/**
831+
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
832+
*/
833+
public function testReadLinkFails()
834+
{
835+
$this->filesystem->readlink($this->workspace.DIRECTORY_SEPARATOR.'invalid');
836+
}
837+
838+
public function testRealPath()
839+
{
840+
$this->markAsSkippedIfSymlinkIsMissing();
841+
842+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
843+
$link1 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'link';
844+
$link2 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'subdir'.DIRECTORY_SEPARATOR.'link';
845+
846+
touch($file);
847+
848+
$this->filesystem->symlink($file, $link1);
849+
$this->filesystem->symlink($link1, $link2);
850+
851+
$this->assertTrue(is_link($link1));
852+
$this->assertEquals($file, $this->filesystem->realpath($link1));
853+
$this->assertTrue(is_link($link2));
854+
$this->assertEquals($file, $this->filesystem->realpath($link2));
855+
}
856+
857+
public function testRealPathNotLink()
858+
{
859+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
860+
861+
touch($file);
862+
863+
$this->assertEquals($file, $this->filesystem->realpath($file));
864+
}
865+
866+
/**
867+
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
868+
*/
869+
public function testRealPathFails()
870+
{
871+
$this->filesystem->realpath($this->workspace.DIRECTORY_SEPARATOR.'invalid');
872+
}
873+
802874
/**
803875
* @dataProvider providePathsForMakePathRelative
804876
*/

0 commit comments

Comments
 (0)
0