From a69279c791966f53ac859f425e80cf005a84969c Mon Sep 17 00:00:00 2001 From: Amrouche Hamza Date: Mon, 27 Nov 2017 07:50:39 +0100 Subject: [PATCH] [Filesystem] Several issues with Filesystem::makePathRelative --- .../Component/Filesystem/Filesystem.php | 28 +- .../Filesystem/Tests/FilesystemTest.php | 548 ++++++++++++++++-- 2 files changed, 513 insertions(+), 63 deletions(-) diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 40371d9307cd..08069de5c10f 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -370,19 +370,25 @@ public function makePathRelative($endPath, $startPath) if ('\\' === DIRECTORY_SEPARATOR) { $endPath = str_replace('\\', '/', $endPath); $startPath = str_replace('\\', '/', $startPath); - } - $stripDriveLetter = function ($path) { - if (strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0])) { - return substr($path, 2); - } + $stripDriveLetter = function ($path) { + if (strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0])) { + return array(substr($path, 2), $path[0].$path[1]); + } - return $path; - }; + return $path; + }; - $endPath = $stripDriveLetter($endPath); - $startPath = $stripDriveLetter($startPath); + $endPath = $stripDriveLetter($endPath); + $startPath = $stripDriveLetter($startPath); + if ($endPath[1] !== $startPath[1]) { + return ''; + } + + $endPath = $endPath[0]; + $startPath = $startPath[0]; + } // Split the paths into arrays $startPathArr = explode('/', trim($startPath, '/')); $endPathArr = explode('/', trim($endPath, '/')); @@ -419,11 +425,13 @@ public function makePathRelative($endPath, $startPath) // Repeated "../" for each level need to reach the common path $traverser = str_repeat('../', $depth); - $endPathRemainder = implode('/', array_slice($endPathArr, $index)); // Construct $endPath from traversing to the common path, then to the remaining $endPath $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : ''); + if ('/' !== substr($endPath, -1)) { + $relativePath = substr($relativePath, 0, -1); + } return '' === $relativePath ? './' : $relativePath; } diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index 3c50131db2f4..aa4db5e20e08 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -449,10 +449,6 @@ public function testChmodWithWrongModLeavesPreviousPermissionsUntouched() { $this->markAsSkippedIfChmodIsMissing(); - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('chmod() changes permissions even when passing invalid modes on HHVM'); - } - $dir = $this->workspace.DIRECTORY_SEPARATOR.'file'; touch($dir); @@ -585,6 +581,23 @@ public function testChownSymlink() $this->assertSame($owner, $this->getFileOwner($link)); } + public function testChownLink() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->hardlink($file, $link); + + $owner = $this->getFileOwner($link); + $this->filesystem->chown($link, $owner); + + $this->assertSame($owner, $this->getFileOwner($link)); + } + /** * @expectedException \Symfony\Component\Filesystem\Exception\IOException */ @@ -602,6 +615,23 @@ public function testChownSymlinkFails() $this->filesystem->chown($link, 'user'.time().mt_rand(1000, 9999)); } + /** + * @expectedException \Symfony\Component\Filesystem\Exception\IOException + */ + public function testChownLinkFails() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->hardlink($file, $link); + + $this->filesystem->chown($link, 'user'.time().mt_rand(1000, 9999)); + } + /** * @expectedException \Symfony\Component\Filesystem\Exception\IOException */ @@ -660,6 +690,23 @@ public function testChgrpSymlink() $this->assertSame($group, $this->getFileGroup($link)); } + public function testChgrpLink() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->hardlink($file, $link); + + $group = $this->getFileGroup($link); + $this->filesystem->chgrp($link, $group); + + $this->assertSame($group, $this->getFileGroup($link)); + } + /** * @expectedException \Symfony\Component\Filesystem\Exception\IOException */ @@ -677,6 +724,23 @@ public function testChgrpSymlinkFails() $this->filesystem->chgrp($link, 'user'.time().mt_rand(1000, 9999)); } + /** + * @expectedException \Symfony\Component\Filesystem\Exception\IOException + */ + public function testChgrpLinkFails() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->hardlink($file, $link); + + $this->filesystem->chgrp($link, 'user'.time().mt_rand(1000, 9999)); + } + /** * @expectedException \Symfony\Component\Filesystem\Exception\IOException */ @@ -828,6 +892,193 @@ public function testSymlinkCreatesTargetDirectoryIfItDoesNotExist() $this->assertEquals($file, readlink($link2)); } + public function testLink() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + $this->filesystem->hardlink($file, $link); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + } + + /** + * @depends testLink + */ + public function testRemoveLink() + { + $this->markAsSkippedIfLinkIsMissing(); + + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + $this->filesystem->remove($link); + + $this->assertTrue(!is_file($link)); + } + + public function testLinkIsOverwrittenIfPointsToDifferentTarget() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $file2 = $this->workspace.DIRECTORY_SEPARATOR.'file2'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + touch($file2); + link($file2, $link); + + $this->filesystem->hardlink($file, $link); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + } + + public function testLinkIsNotOverwrittenIfAlreadyCreated() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + link($file, $link); + + $this->filesystem->hardlink($file, $link); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + } + + public function testLinkWithSeveralTargets() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link1 = $this->workspace.DIRECTORY_SEPARATOR.'link'; + $link2 = $this->workspace.DIRECTORY_SEPARATOR.'link2'; + + touch($file); + + $this->filesystem->hardlink($file, array($link1, $link2)); + + $this->assertTrue(is_file($link1)); + $this->assertEquals(fileinode($file), fileinode($link1)); + $this->assertTrue(is_file($link2)); + $this->assertEquals(fileinode($file), fileinode($link2)); + } + + public function testLinkWithSameTarget() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + // practically same as testLinkIsNotOverwrittenIfAlreadyCreated + $this->filesystem->hardlink($file, array($link, $link)); + + $this->assertTrue(is_file($link)); + $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 */ @@ -843,62 +1094,71 @@ public function providePathsForMakePathRelative() $paths = array( array('/var/lib/symfony/src/Symfony/', '/var/lib/symfony/src/Symfony/Component', '../'), array('/var/lib/symfony/src/Symfony/', '/var/lib/symfony/src/Symfony/Component/', '../'), - array('/var/lib/symfony/src/Symfony', '/var/lib/symfony/src/Symfony/Component', '../'), - array('/var/lib/symfony/src/Symfony', '/var/lib/symfony/src/Symfony/Component/', '../'), + array('/var/lib/symfony/src/Symfony/', '/var/lib/symfony/src/Symfony/Component', '../'), + array('/var/lib/symfony/src/Symfony/', '/var/lib/symfony/src/Symfony/Component/', '../'), array('var/lib/symfony/', 'var/lib/symfony/src/Symfony/Component', '../../../'), array('/usr/lib/symfony/', '/var/lib/symfony/src/Symfony/Component', '../../../../../../usr/lib/symfony/'), array('usr/lib/symfony/', 'var/lib/symfony/src/Symfony/Component', '../../../../../../usr/lib/symfony/'), array('/var/lib/symfony/src/Symfony/', '/var/lib/symfony/', 'src/Symfony/'), - array('/aa/bb', '/aa/bb', './'), - array('/aa/bb', '/aa/bb/', './'), array('/aa/bb/', '/aa/bb', './'), array('/aa/bb/', '/aa/bb/', './'), - array('/aa/bb/cc', '/aa/bb/cc/dd', '../'), - array('/aa/bb/cc', '/aa/bb/cc/dd/', '../'), + array('/aa/bb/', '/aa/bb', './'), + array('/aa/bb/', '/aa/bb/', './'), array('/aa/bb/cc/', '/aa/bb/cc/dd', '../'), array('/aa/bb/cc/', '/aa/bb/cc/dd/', '../'), - array('/aa/bb/cc', '/aa', 'bb/cc/'), - array('/aa/bb/cc', '/aa/', 'bb/cc/'), + array('/aa/bb/cc/', '/aa/bb/cc/dd', '../'), + array('/aa/bb/cc/', '/aa/bb/cc/dd/', '../'), + array('/aa/bb/cc/', '/aa', 'bb/cc/'), + array('/aa/bb/cc/', '/aa/', 'bb/cc/'), array('/aa/bb/cc/', '/aa', 'bb/cc/'), array('/aa/bb/cc/', '/aa/', 'bb/cc/'), - array('/a/aab/bb', '/a/aa', '../aab/bb/'), - array('/a/aab/bb', '/a/aa/', '../aab/bb/'), + array('/a/aab/bb/', '/a/aa', '../aab/bb/'), + array('/a/aab/bb/', '/a/aa/', '../aab/bb/'), array('/a/aab/bb/', '/a/aa', '../aab/bb/'), array('/a/aab/bb/', '/a/aa/', '../aab/bb/'), array('/a/aab/bb/', '/', 'a/aab/bb/'), array('/a/aab/bb/', '/b/aab', '../../a/aab/bb/'), - array('/aab/bb', '/aa', '../aab/bb/'), - array('/aab', '/aa', '../aab/'), - array('/aa/bb/cc', '/aa/dd/..', 'bb/cc/'), - array('/aa/../bb/cc', '/aa/dd/..', '../bb/cc/'), - array('/aa/bb/../../cc', '/aa/../dd/..', 'cc/'), - array('/../aa/bb/cc', '/aa/dd/..', 'bb/cc/'), - array('/../../aa/../bb/cc', '/aa/dd/..', '../bb/cc/'), - array('C:/aa/bb/cc', 'C:/aa/dd/..', 'bb/cc/'), - array('c:/aa/../bb/cc', 'c:/aa/dd/..', '../bb/cc/'), - array('C:/aa/bb/../../cc', 'C:/aa/../dd/..', 'cc/'), - array('C:/../aa/bb/cc', 'C:/aa/dd/..', 'bb/cc/'), - array('C:/../../aa/../bb/cc', 'C:/aa/dd/..', '../bb/cc/'), - array('aa/bb', 'aa/cc', '../bb/'), - array('aa/cc', 'bb/cc', '../../aa/cc/'), - array('aa/bb', 'aa/./cc', '../bb/'), - array('aa/./bb', 'aa/cc', '../bb/'), - array('aa/./bb', 'aa/./cc', '../bb/'), - array('../../', '../../', './'), - array('../aa/bb/', 'aa/bb/', '../../../aa/bb/'), - array('../../../', '../../', '../'), - array('', '', './'), - array('', 'aa/', '../'), - array('aa/', '', 'aa/'), + array('/aab/bb/', '/aa', '../aab/bb/'), + array('/aab/', '/aa', '../aab/'), + array('/aa/bb/cc/', '/aa/dd/..', 'bb/cc/'), + array('/aa/../bb/cc/', '/aa/dd/..', '../bb/cc/'), + array('/aa/bb/../../cc/', '/aa/../dd/..', 'cc/'), + array('/../aa/bb/cc/', '/aa/dd/..', 'bb/cc/'), + array('/../../aa/../bb/cc/', '/aa/dd/..', '../bb/cc/'), + array('C:/aa/bb/cc/', 'C:/aa/dd/..', 'bb/cc/'), + array('c:/aa/../bb/cc/', 'c:/aa/dd/..', '../bb/cc/'), + array('C:/aa/bb/../../cc/', 'C:/aa/../dd/..', 'cc/'), + array('C:/../aa/bb/cc/', 'C:/aa/dd/..', '../../aa/bb/cc/'), + array('C:/../../aa/../bb/cc/', 'C:/aa/dd/..', '../../bb/cc/'), + array('/aa/bb/cc', '/aa', 'bb/cc'), ); if ('\\' === DIRECTORY_SEPARATOR) { $paths[] = array('c:\var\lib/symfony/src/Symfony/', 'c:/var/lib/symfony/', 'src/Symfony/'); + $paths[] = array('C:\aa\bb\cc', 'D:\aa\dd\\', ''); } return $paths; } + /** + * @expectedException \Symfony\Component\Filesystem\Exception\InvalidArgumentException + * @expectedExceptionMessage The start path "var/lib/symfony/src/Symfony/Component" is not absolute. + */ + public function testMakePathRelativeWithRelativeStartPath() + { + $this->assertSame('../../../', $this->filesystem->makePathRelative('/var/lib/symfony/', 'var/lib/symfony/src/Symfony/Component')); + } + + /** + * @expectedException \Symfony\Component\Filesystem\Exception\InvalidArgumentException + * @expectedExceptionMessage The end path "var/lib/symfony/" is not absolute. + */ + public function testMakePathRelativeWithRelativeEndPath() + { + $this->assertSame('../../../', $this->filesystem->makePathRelative('var/lib/symfony/', '/var/lib/symfony/src/Symfony/Component')); + } + public function testMirrorCopiesFilesAndDirectoriesRecursively() { $sourcePath = $this->workspace.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR; @@ -1088,46 +1348,126 @@ public function providePathsForIsAbsolutePath() ); } - public function testDumpFile() + public function testTempnam() { - $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; + $dirname = $this->workspace; - $this->filesystem->dumpFile($filename, 'bar'); + $filename = $this->filesystem->tempnam($dirname, 'foo'); $this->assertFileExists($filename); - $this->assertSame('bar', file_get_contents($filename)); + } + + public function testTempnamWithFileScheme() + { + $scheme = 'file://'; + $dirname = $scheme.$this->workspace; + + $filename = $this->filesystem->tempnam($dirname, 'foo'); + + $this->assertStringStartsWith($scheme, $filename); + $this->assertFileExists($filename); + } + + public function testTempnamWithMockScheme() + { + stream_wrapper_register('mock', 'Symfony\Component\Filesystem\Tests\Fixtures\MockStream\MockStream'); + + $scheme = 'mock://'; + $dirname = $scheme.$this->workspace; + + $filename = $this->filesystem->tempnam($dirname, 'foo'); + + $this->assertStringStartsWith($scheme, $filename); + $this->assertFileExists($filename); } /** - * @group legacy + * @expectedException \Symfony\Component\Filesystem\Exception\IOException */ - public function testDumpFileAndSetPermissions() + public function testTempnamWithZlibSchemeFails() { - $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; + $scheme = 'compress.zlib://'; + $dirname = $scheme.$this->workspace; - $this->filesystem->dumpFile($filename, 'bar', 0753); + // The compress.zlib:// stream does not support mode x: creates the file, errors "failed to open stream: operation failed" and returns false + $this->filesystem->tempnam($dirname, 'bar'); + } - $this->assertFileExists($filename); - $this->assertSame('bar', file_get_contents($filename)); + public function testTempnamWithPHPTempSchemeFails() + { + $scheme = 'php://temp'; + $dirname = $scheme; - // skip mode check on Windows - if ('\\' !== DIRECTORY_SEPARATOR) { - $this->assertFilePermissions(753, $filename); + $filename = $this->filesystem->tempnam($dirname, 'bar'); + + $this->assertStringStartsWith($scheme, $filename); + + // The php://temp stream deletes the file after close + $this->assertFileNotExists($filename); + } + + /** + * @expectedException \Symfony\Component\Filesystem\Exception\IOException + */ + public function testTempnamWithPharSchemeFails() + { + // Skip test if Phar disabled phar.readonly must be 0 in php.ini + if (!\Phar::canWrite()) { + $this->markTestSkipped('This test cannot run when phar.readonly is 1.'); } + + $scheme = 'phar://'; + $dirname = $scheme.$this->workspace; + $pharname = 'foo.phar'; + + new \Phar($this->workspace.'/'.$pharname, 0, $pharname); + // The phar:// stream does not support mode x: fails to create file, errors "failed to open stream: phar error: "$filename" is not a file in phar "$pharname"" and returns false + $this->filesystem->tempnam($dirname, $pharname.'/bar'); + } + + /** + * @expectedException \Symfony\Component\Filesystem\Exception\IOException + */ + public function testTempnamWithHTTPSchemeFails() + { + $scheme = 'http://'; + $dirname = $scheme.$this->workspace; + + // The http:// scheme is read-only + $this->filesystem->tempnam($dirname, 'bar'); } - public function testDumpFileWithNullMode() + public function testTempnamOnUnwritableFallsBackToSysTmp() + { + $scheme = 'file://'; + $dirname = $scheme.$this->workspace.DIRECTORY_SEPARATOR.'does_not_exist'; + + $filename = $this->filesystem->tempnam($dirname, 'bar'); + $realTempDir = realpath(sys_get_temp_dir()); + $this->assertStringStartsWith(rtrim($scheme.$realTempDir, DIRECTORY_SEPARATOR), $filename); + $this->assertFileExists($filename); + + // Tear down + @unlink($filename); + } + + public function testDumpFile() { $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; - $this->filesystem->dumpFile($filename, 'bar', null); + // skip mode check on Windows + if ('\\' !== DIRECTORY_SEPARATOR) { + $oldMask = umask(0002); + } + $this->filesystem->dumpFile($filename, 'bar'); $this->assertFileExists($filename); $this->assertSame('bar', file_get_contents($filename)); // skip mode check on Windows if ('\\' !== DIRECTORY_SEPARATOR) { - $this->assertFilePermissions(600, $filename); + $this->assertFilePermissions(664, $filename); + umask($oldMask); } } @@ -1142,6 +1482,100 @@ public function testDumpFileOverwritesAnExistingFile() $this->assertSame('bar', file_get_contents($filename)); } + public function testDumpFileWithFileScheme() + { + $scheme = 'file://'; + $filename = $scheme.$this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; + + $this->filesystem->dumpFile($filename, 'bar'); + + $this->assertFileExists($filename); + $this->assertSame('bar', file_get_contents($filename)); + } + + public function testDumpFileWithZlibScheme() + { + $scheme = 'compress.zlib://'; + $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; + + $this->filesystem->dumpFile($filename, 'bar'); + + // Zlib stat uses file:// wrapper so remove scheme + $this->assertFileExists(str_replace($scheme, '', $filename)); + $this->assertSame('bar', file_get_contents($filename)); + } + + public function testAppendToFile() + { + $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'bar.txt'; + + // skip mode check on Windows + if ('\\' !== DIRECTORY_SEPARATOR) { + $oldMask = umask(0002); + } + + $this->filesystem->dumpFile($filename, 'foo'); + + $this->filesystem->appendToFile($filename, 'bar'); + + $this->assertFileExists($filename); + $this->assertSame('foobar', file_get_contents($filename)); + + // skip mode check on Windows + if ('\\' !== DIRECTORY_SEPARATOR) { + $this->assertFilePermissions(664, $filename, 'The written file should keep the same permissions as before.'); + umask($oldMask); + } + } + + public function testAppendToFileWithScheme() + { + $scheme = 'file://'; + $filename = $scheme.$this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; + $this->filesystem->dumpFile($filename, 'foo'); + + $this->filesystem->appendToFile($filename, 'bar'); + + $this->assertFileExists($filename); + $this->assertSame('foobar', file_get_contents($filename)); + } + + public function testAppendToFileWithZlibScheme() + { + $scheme = 'compress.zlib://'; + $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt'; + $this->filesystem->dumpFile($filename, 'foo'); + + // Zlib stat uses file:// wrapper so remove it + $this->assertSame('foo', file_get_contents(str_replace($scheme, '', $filename))); + + $this->filesystem->appendToFile($filename, 'bar'); + + $this->assertFileExists($filename); + $this->assertSame('foobar', file_get_contents($filename)); + } + + public function testAppendToFileCreateTheFileIfNotExists() + { + $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'bar.txt'; + + // skip mode check on Windows + if ('\\' !== DIRECTORY_SEPARATOR) { + $oldMask = umask(0002); + } + + $this->filesystem->appendToFile($filename, 'bar'); + + // skip mode check on Windows + if ('\\' !== DIRECTORY_SEPARATOR) { + $this->assertFilePermissions(664, $filename); + umask($oldMask); + } + + $this->assertFileExists($filename); + $this->assertSame('bar', file_get_contents($filename)); + } + public function testDumpKeepsExistingPermissionsWhenOverwritingAnExistingFile() { $this->markAsSkippedIfChmodIsMissing(); @@ -1169,4 +1603,12 @@ public function testCopyShouldKeepExecutionPermission() $this->assertFilePermissions(767, $targetFilePath); } + + /** + * Normalize the given path (transform each blackslash into a real directory separator). + */ + private function normalize(string $path): string + { + return str_replace('/', DIRECTORY_SEPARATOR, $path); + } }