8000 [Filesystem] Add a cross-platform readlink/realpath methods for neste… · symfony/symfony@fa3b7fd · GitHub
[go: up one dir, main page]

Skip to content

Commit fa3b7fd

Browse files
committed
[Filesystem] Add a cross-platform readlink/realpath methods for nested links
1 parent ad85c79 commit fa3b7fd

File tree

3 files changed

+140
-6
lines changed

3 files changed

+140
-6
lines changed

src/Symfony/Component/Filesystem/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
3.2.0
5+
-----
6+
7+
* added `readlink()` as a platform independent method to read links
8+
49
3.0.0
510
-----
611

src/Symfony/Component/Filesystem/Filesystem.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,53 @@ private function linkException($origin, $target, $linkType)
383383
throw new IOException(sprintf('Failed to create %s link from "%s" to "%s".', $linkType, $origin, $target), 0, null, $target);
384384
}
385385

386+
/**
387+
* Resolves links in paths.
388+
*
389+
* With $canonicalize = false
390+
* - if $path does not exist or is not a link, returns null
391+
* - if $path is a link, returns the next direct target of the link without considering the existence of the target
392+
*
393+
* With $canonicalize = true
394+
* - if $path does not exist or is not a link, returns null
395+
* - if $path exists, returns its absolute fully resolved final version
396+
*
397+
* @param string $path A filesystem path
398+
* @param bool $canonicalize Whether or not to return a canonicalized path
399+
*
400+
* @return string|null
401+
*/
402+
public function readlink($path, $canonicalize = false)
403+
{
404+
if (!$canonicalize && !is_link($path)) {
405+
return;
406+
}
407+
408+
// /link1 -> /link2 -> /file
409+
410+
// Windows: readlink(/link1) => /file
411+
// realpath(/link1) => /link2
412+
413+
// Unix: readlink(/link1) => /link2
414+
// realpath(/link1) => /file
415+
416+
if ($canonicalize) {
417+
if ('\\' === DIRECTORY_SEPARATOR) {
418+
$target = realpath(readlink($path));
419+
} else {
420+
$target = realpath($path);
421+
}
422+
423+
return $this->exists($target) ? $target : null;
424+
}
425+
426+
if ('\\' === DIRECTORY_SEPARATOR) {
427+
return realpath($path);
428+
}
429+
430+
return readlink($path);
431+
}
432+
386433
/**
387434
* Given an existing path, convert it to a path relative to a given starting path.
388435
*

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

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -957,14 +957,84 @@ public function testLinkWithSameTarget()
957957
$this->assertEquals(fileinode($file), fileinode($link));
958958
}
959959

960-
/**
961-
* @dataProvider providePathsForMakePathRelative
962-
*/
963-
public function testMakePathRelative($endPath, $startPath, $expectedPath)
960+
public function testReadRelativeLink()
961+
{
962+
$this->markAsSkippedIfSymlinkIsMissing();
963+
964+
if ('\\' === DIRECTORY_SEPARATOR) {
965+
$this->markTestSkipped('Relative symbolic links are not supported on Windows');
966+
}
967+
968+
$file = $this->normalize($this->workspace.'/file');
969+
$link1 = $this->normalize($this->workspace.'/dir/link');
970+
$link2 = $this->normalize($this->workspace.'/dir/link2');
971+
touch($file);
972+
973+
$this->filesystem->symlink($this->normalize('../file'), $link1);
974+
$this->filesystem->symlink('link', $link2);
975+
976+
$this->assertEquals($this->normalize('../file'), $this->filesystem->readlink($link1));
977+
$this->assertEquals('link', $this->filesystem->readlink($link2));
978+
$this->assertEquals($file, $this->filesystem->readlink($link1, true));
979+
$this->assertEquals($file, $this->filesystem->readlink($link2, true));
980+
$this->assertEquals($file, $this->filesystem->readlink($file, true));
981+
}
982+
983+
public function testReadBrokenLink()
984+
{
985+
$this->markAsSkippedIfSymlinkIsMissing();
986+
987+
if ('\\' === DIRECTORY_SEPARATOR) {
988+
$this->markTestSkipped('Windows does not support creating "broken" symlinks');
989+
}
990+
991+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
992+
$link = $this->workspace.DIRECTORY_SEPARATOR.'link';
993+
994+
$this->filesystem->symlink($file, $link);
995+
996+
$this->assertEquals($file, $this->filesystem->readlink($link));
997+
$this->assertNull($this->filesystem->readlink($link, true));
998+
999+
touch($file);
1000+
$this->assertEquals($file, $this->filesystem->readlink($link, true));
1001+
}
1002+
1003+
public function testReadAbsoluteLink()
9641004
{
965-
$path = $this->filesystem->makePathRelative($endPath, $startPath);
1005+
$this->markAsSkippedIfSymlinkIsMissing();
1006+
1007+
$file = $this->normalize($this->workspace.'/file');
1008+
$link1 = $this->normalize($this->workspace.'/dir/link');
1009+
$link2 = $this->normalize($this->workspace.'/dir/link2');
1010+
touch($file);
9661011

967-
$this->assertEquals($expectedPath, $path);
1012+
$this->filesystem->symlink($file, $link1);
1013+
$this->filesystem->symlink($link1, $link2);
1014+
1015+
$this->assertEquals($file, $this->filesystem->readlink($link1));
1016+
$this->assertEquals($link1, $this->filesystem->readlink($link2));
1017+
$this->assertEquals($file, $this->filesystem->readlink($link1, true));
1018+
$this->assertEquals($file, $this->filesystem->readlink($link2, true));
1019+
$this->assertEquals($file, $this->filesystem->readlink($file, true));
1020+
}
1021+
1022+
public function testReadLinkDefaultPathDoesNotExist()
1023+
{
1024+
$this->assertNull($this->filesystem->readlink($this->normalize($this->workspace.'/invalid')));
1025+
}
1026+
1027+
public function testReadLinkDefaultPathNotLink()
1028+
{
1029+
$file = $this->normalize($this->workspace.'/file');
1030+
touch($file);
1031+
1032+
$this->assertNull($this->filesystem->readlink($file));
1033+
}
1034+
1035+
public function testReadLinkCanonicalizedPathDoesNotExist()
1036+
{
1037+
$this->assertNull($this->filesystem->readlink($this->normalize($this->workspace.'invalid'), true));
9681038
}
9691039

9701040
/**
@@ -1321,4 +1391,16 @@ public function testCopyShouldKeepExecutionPermission()
13211391

13221392
$this->assertFilePermissions(767, $targetFilePath);
13231393
}
1394+
1395+
/**
1396+
* Normalize the given path (transform each blackslash into a real directory separator).
1397+
*
1398+
* @param string $path
1399+
*
1400+
* @return string
1401+
*/
1402+
private function normalize($path)
1403+
{
1404+
return str_replace('/', DIRECTORY_SEPARATOR, $path);
1405+
}
13241406
}

0 commit comments

Comments
 (0)
0