From 4ccce1620b23004348991d38558842a840b33c8e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 May 2023 17:24:39 +0200 Subject: [PATCH 01/15] [7.0] Bump to PHP 8.2 minimum --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 10a7a531c..1e054b682 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, From 900b2f14de4cf857cc52c503ad4cb7d09a2a523e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 30 Jun 2023 18:53:02 +0200 Subject: [PATCH 02/15] Remove BC layers related to new methods and new parameters --- CHANGELOG.md | 5 +++++ Filesystem.php | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb7170ca..b4bd22eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.0 +--- + + * Add argument `$lock` to `Filesystem::appendToFile()` + 5.4 --- diff --git a/Filesystem.php b/Filesystem.php index a379ce186..55db9d91a 100644 --- a/Filesystem.php +++ b/Filesystem.php @@ -703,7 +703,7 @@ public function dumpFile(string $filename, $content) * * @throws IOException If the file is not writable */ - public function appendToFile(string $filename, $content/* , bool $lock = false */) + public function appendToFile(string $filename, $content, bool $lock = false) { if (\is_array($content)) { throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); @@ -715,8 +715,6 @@ public function appendToFile(string $filename, $content/* , bool $lock = false * $this->mkdir($dir); } - $lock = \func_num_args() > 2 && func_get_arg(2); - if (false === self::box('file_put_contents', $filename, $content, \FILE_APPEND | ($lock ? \LOCK_EX : 0))) { throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename); } From 6f6ec0d053bd94f42a2508747ce80b2e7074526f Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 2 Jul 2023 23:52:21 +0200 Subject: [PATCH 03/15] [Components] Convert to native return types --- Filesystem.php | 52 +++++++++++++------------------------------------- 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/Filesystem.php b/Filesystem.php index 55db9d91a..9aaa32a55 100644 --- a/Filesystem.php +++ b/Filesystem.php @@ -31,12 +31,10 @@ class Filesystem * If the target file is newer, it is overwritten only when the * $overwriteNewerFiles option is set to true. * - * @return void - * * @throws FileNotFoundException When originFile doesn't exist * @throws IOException When copy fails */ - public function copy(string $originFile, string $targetFile, bool $overwriteNewerFiles = false) + public function copy(string $originFile, string $targetFile, bool $overwriteNewerFiles = false): void { $originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://'); if ($originIsLocal && !is_file($originFile)) { @@ -84,11 +82,9 @@ public function copy(string $originFile, string $targetFile, bool $overwriteNewe /** * Creates a directory recursively. * - * @return void - * * @throws IOException On any directory creation failure */ - public function mkdir(string|iterable $dirs, int $mode = 0777) + public function mkdir(string|iterable $dirs, int $mode = 0777): void { foreach ($this->toIterable($dirs) as $dir) { if (is_dir($dir)) { @@ -127,11 +123,9 @@ public function exists(string|iterable $files): bool * @param int|null $time The touch time as a Unix timestamp, if not supplied the current system time is used * @param int|null $atime The access time as a Unix timestamp, if not supplied the current system time is used * - * @return void - * * @throws IOException When touch fails */ - public function touch(string|iterable $files, int $time = null, int $atime = null) + public function touch(string|iterable $files, int $time = null, int $atime = null): void { foreach ($this->toIterable($files) as $file) { if (!($time ? self::box('touch', $file, $time, $atime) : self::box('touch', $file))) { @@ -143,11 +137,9 @@ public function touch(string|iterable $files, int $time = null, int $atime = nul /** * Removes files or directories. * - * @return void - * * @throws IOException When removal fails */ - public function remove(string|iterable $files) + public function remove(string|iterable $files): void { if ($files instanceof \Traversable) { $files = iterator_to_array($files, false); @@ -211,11 +203,9 @@ private static function doRemove(array $files, bool $isRecursive): void * @param int $umask The mode mask (octal) * @param bool $recursive Whether change the mod recursively or not * - * @return void - * * @throws IOException When the change fails */ - public function chmod(string|iterable $files, int $mode, int $umask = 0000, bool $recursive = false) + public function chmod(string|iterable $files, int $mode, int $umask = 0000, bool $recursive = false): void { foreach ($this->toIterable($files) as $file) { if (!self::box('chmod', $file, $mode & ~$umask)) { @@ -233,11 +223,9 @@ public function chmod(string|iterable $files, int $mode, int $umask = 0000, bool * @param string|int $user A user name or number * @param bool $recursive Whether change the owner recursively or not * - * @return void - * * @throws IOException When the change fails */ - public function chown(string|iterable $files, string|int $user, bool $recursive = false) + public function chown(string|iterable $files, string|int $user, bool $recursive = false): void { foreach ($this->toIterable($files) as $file) { if ($recursive && is_dir($file) && !is_link($file)) { @@ -261,11 +249,9 @@ public function chown(string|iterable $files, string|int $user, bool $recursive * @param string|int $group A group name or number * @param bool $recursive Whether change the group recursively or not * - * @return void - * * @throws IOException When the change fails */ - public function chgrp(string|iterable $files, string|int $group, bool $recursive = false) + public function chgrp(string|iterable $files, string|int $group, bool $recursive = false): void { foreach ($this->toIterable($files) as $file) { if ($recursive && is_dir($file) && !is_link($file)) { @@ -286,12 +272,10 @@ public function chgrp(string|iterable $files, string|int $group, bool $recursive /** * Renames a file or a directory. * - * @return void - * * @throws IOException When target file or directory already exists * @throws IOException When origin cannot be renamed */ - public function rename(string $origin, string $target, bool $overwrite = false) + public function rename(string $origin, string $target, bool $overwrite = false): void { // we check that target does not exist if (!$overwrite && $this->isReadable($target)) { @@ -329,11 +313,9 @@ private function isReadable(string $filename): bool /** * Creates a symbolic link or copy a directory. * - * @return void - * * @throws IOException When symlink fails */ - public function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false) + public function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false): void { self::assertFunctionExists('symlink'); @@ -367,12 +349,10 @@ public function symlink(string $originDir, string $targetDir, bool $copyOnWindow * * @param string|string[] $targetFiles The target file(s) * - * @return void - * * @throws FileNotFoundException When original file is missing or not a file * @throws IOException When link fails, including if link already exists */ - public function hardlink(string $originFile, string|iterable $targetFiles) + public function hardlink(string $originFile, string|iterable $targetFiles): void { self::assertFunctionExists('link'); @@ -526,11 +506,9 @@ public function makePathRelative(string $endPath, string $startPath): string * - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false) * - $options['delete'] Whether to delete files that are not in the source directory (defaults to false) * - * @return void - * * @throws IOException When file type is unknown */ - public function mirror(string $originDir, string $targetDir, \Traversable $iterator = null, array $options = []) + public function mirror(string $originDir, string $targetDir, \Traversable $iterator = null, array $options = []): void { $targetDir = rtrim($targetDir, '/\\'); $originDir = rtrim($originDir, '/\\'); @@ -652,11 +630,9 @@ public function tempnam(string $dir, string $prefix, string $suffix = ''): strin * * @param string|resource $content The data to write into the file * - * @return void - * * @throws IOException if the file cannot be written to */ - public function dumpFile(string $filename, $content) + public function dumpFile(string $filename, $content): void { if (\is_array($content)) { throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); @@ -699,11 +675,9 @@ public function dumpFile(string $filename, $content) * @param string|resource $content The content to append * @param bool $lock Whether the file should be locked when writing to it * - * @return void - * * @throws IOException If the file is not writable */ - public function appendToFile(string $filename, $content, bool $lock = false) + public function appendToFile(string $filename, $content, bool $lock = false): void { if (\is_array($content)) { throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); From ef0127112da79e0c2052bcdac399dec6dbfb8550 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 14 Dec 2023 11:03:37 +0100 Subject: [PATCH 04/15] Set `strict` parameter of `in_array` to true where possible --- Path.php | 2 +- Tests/FilesystemTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Path.php b/Path.php index 664396235..571cc7b70 100644 --- a/Path.php +++ b/Path.php @@ -668,7 +668,7 @@ public static function join(string ...$paths): string } // Only add slash if previous part didn't end with '/' or '\' - if (!\in_array(substr($finalPath, -1), ['/', '\\'])) { + if (!\in_array(substr($finalPath, -1), ['/', '\\'], true)) { $finalPath .= '/'; } diff --git a/Tests/FilesystemTest.php b/Tests/FilesystemTest.php index 2c222fd06..dc7e74c84 100644 --- a/Tests/FilesystemTest.php +++ b/Tests/FilesystemTest.php @@ -167,7 +167,7 @@ public function testCopyCreatesTargetDirectoryIfItDoesNotExist() */ public function testCopyForOriginUrlsAndExistingLocalFileDefaultsToCopy() { - if (!\in_array('https', stream_get_wrappers())) { + if (!\in_array('https', stream_get_wrappers(), true)) { $this->markTestSkipped('"https" stream wrapper is not enabled.'); } $sourceFilePath = 'https://symfony.com/images/common/logo/logo_symfony_header.png'; From c5254cde56e050a391adb77cb5b1d99638b1e99a Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 18 Dec 2023 08:46:12 +0100 Subject: [PATCH 05/15] Code updates --- Path.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Path.php b/Path.php index 664396235..3aa1910ec 100644 --- a/Path.php +++ b/Path.php @@ -346,13 +346,13 @@ public static function changeExtension(string $path, string $extension): string $extension = ltrim($extension, '.'); // No extension for paths - if ('/' === substr($path, -1)) { + if (str_ends_with($path, '/')) { return $path; } // No actual extension in path if (empty($actualExtension)) { - return $path.('.' === substr($path, -1) ? '' : '.').$extension; + return $path.(str_ends_with($path, '.') ? '' : '.').$extension; } return substr($path, 0, -\strlen($actualExtension)).$extension; From 293b483723cc18b7deaf0119cfe6f8bed210e98c Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 10 Jan 2024 17:41:00 +0100 Subject: [PATCH 06/15] [Filesystem][Finder][Form] Use CPP --- Exception/IOException.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Exception/IOException.php b/Exception/IOException.php index a3c544553..f85207257 100644 --- a/Exception/IOException.php +++ b/Exception/IOException.php @@ -20,12 +20,12 @@ */ class IOException extends \RuntimeException implements IOExceptionInterface { - private ?string $path; - - public function __construct(string $message, int $code = 0, \Throwable $previous = null, string $path = null) - { - $this->path = $path; - + public function __construct( + string $message, + int $code = 0, + \Throwable $previous = null, + private ?string $path = null, + ) { parent::__construct($message, $code, $previous); } From 40f7a3ad8311290f092c2940c642e10464536aee Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Wed, 6 Mar 2024 11:55:15 +0100 Subject: [PATCH 07/15] [Filesystem] Add the readFile() method --- CHANGELOG.md | 5 +++++ Filesystem.php | 19 +++++++++++++++++++ Tests/FilesystemTest.php | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4bd22eb5..80818d1b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add the `Filesystem::readFile()` method + 7.0 --- diff --git a/Filesystem.php b/Filesystem.php index 81b508b1e..42e64630e 100644 --- a/Filesystem.php +++ b/Filesystem.php @@ -694,6 +694,25 @@ public function appendToFile(string $filename, $content, bool $lock = false): vo } } + /** + * Returns the content of a file as a string. + * + * @throws IOException If the file cannot be read + */ + public function readFile(string $filename): string + { + if (is_dir($filename)) { + throw new IOException(sprintf('Failed to read file "%s": File is a directory.', $filename)); + } + + $content = self::box('file_get_contents', $filename); + if (false === $content) { + throw new IOException(sprintf('Failed to read file "%s": ', $filename).self::$lastError, 0, null, $filename); + } + + return $content; + } + private function toIterable(string|iterable $files): iterable { return is_iterable($files) ? $files : [$files]; diff --git a/Tests/FilesystemTest.php b/Tests/FilesystemTest.php index dc7e74c84..a1b3a6661 100644 --- a/Tests/FilesystemTest.php +++ b/Tests/FilesystemTest.php @@ -1802,6 +1802,43 @@ public function testDumpKeepsExistingPermissionsWhenOverwritingAnExistingFile() $this->assertFilePermissions(745, $filename); } + public function testReadFile() + { + $licenseFile = \dirname(__DIR__).'/LICENSE'; + + $this->assertStringEqualsFile($licenseFile, $this->filesystem->readFile($licenseFile)); + } + + public function testReadNonExistentFile() + { + $this->expectException(IOException::class); + $this->expectExceptionMessageMatches('#^Failed to read file ".+/Tests/invalid"\\: file_get_contents\\(.+/Tests/invalid\\)\\: Failed to open stream\\: No such file or directory$#'); + + $this->filesystem->readFile(__DIR__.'/invalid'); + } + + public function testReadDirectory() + { + $this->expectException(IOException::class); + $this->expectExceptionMessageMatches('#^Failed to read file ".+/Tests"\\: File is a directory\\.$#'); + + $this->filesystem->readFile(__DIR__); + } + + public function testReadUnreadableFile() + { + $this->markAsSkippedIfChmodIsMissing(); + + $filename = $this->workspace.'/unreadable.txt'; + file_put_contents($filename, 'Hello World'); + chmod($filename, 0o000); + + $this->expectException(IOException::class); + $this->expectExceptionMessageMatches('#^Failed to read file ".+/unreadable.txt"\\: file_get_contents\\(.+/unreadable.txt\\)\\: Failed to open stream\\: Permission denied$#'); + + $this->filesystem->readFile($filename); + } + public function testCopyShouldKeepExecutionPermission() { $this->markAsSkippedIfChmodIsMissing(); From 69b75b9bbb1484285c0609ae4a94ac2d1ee0adf2 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 24 Mar 2024 22:18:03 +0100 Subject: [PATCH 08/15] fix tests on Windows --- Tests/FilesystemTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/FilesystemTest.php b/Tests/FilesystemTest.php index a1b3a6661..8bba20e4a 100644 --- a/Tests/FilesystemTest.php +++ b/Tests/FilesystemTest.php @@ -1812,7 +1812,7 @@ public function testReadFile() public function testReadNonExistentFile() { $this->expectException(IOException::class); - $this->expectExceptionMessageMatches('#^Failed to read file ".+/Tests/invalid"\\: file_get_contents\\(.+/Tests/invalid\\)\\: Failed to open stream\\: No such file or directory$#'); + $this->expectExceptionMessageMatches(sprintf('#^Failed to read file ".+%1$sTests/invalid"\\: file_get_contents\\(.+%1$sTests/invalid\\)\\: Failed to open stream\\: No such file or directory$#', preg_quote(\DIRECTORY_SEPARATOR))); $this->filesystem->readFile(__DIR__.'/invalid'); } @@ -1820,7 +1820,7 @@ public function testReadNonExistentFile() public function testReadDirectory() { $this->expectException(IOException::class); - $this->expectExceptionMessageMatches('#^Failed to read file ".+/Tests"\\: File is a directory\\.$#'); + $this->expectExceptionMessageMatches(sprintf('#^Failed to read file ".+%sTests"\\: File is a directory\\.$#', preg_quote(\DIRECTORY_SEPARATOR))); $this->filesystem->readFile(__DIR__); } From bddc353b1ad64504936f18217474f5dc21295849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Ostroluck=C3=BD?= Date: Sun, 31 Mar 2024 15:15:18 +0200 Subject: [PATCH 09/15] Remove unnecessary empty usages --- Path.php | 2 +- Tests/FilesystemTestCase.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Path.php b/Path.php index 80c616049..bbfd8a247 100644 --- a/Path.php +++ b/Path.php @@ -351,7 +351,7 @@ public static function changeExtension(string $path, string $extension): string } // No actual extension in path - if (empty($actualExtension)) { + if (!$actualExtension) { return $path.(str_ends_with($path, '.') ? '' : '.').$extension; } diff --git a/Tests/FilesystemTestCase.php b/Tests/FilesystemTestCase.php index 8ac8bf5b3..417d665cc 100644 --- a/Tests/FilesystemTestCase.php +++ b/Tests/FilesystemTestCase.php @@ -65,7 +65,7 @@ protected function setUp(): void protected function tearDown(): void { - if (!empty($this->longPathNamesWindows)) { + if ($this->longPathNamesWindows) { foreach ($this->longPathNamesWindows as $path) { exec('DEL '.$path); } From aa99a870bb99e61e8bed835bf90a65d205a9831b Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 3 Apr 2024 17:07:00 +0200 Subject: [PATCH 10/15] update constraint --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d48ad1b72..883a34df3 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8", - "symfony/process": "^5.4|^6.4" + "symfony/process": "^6.4|^7.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Filesystem\\": "" }, From 49519d96d2a6b1670f92eee981156ba0287e906f Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 20 Jun 2024 17:52:34 +0200 Subject: [PATCH 11/15] Prefix all sprintf() calls --- Exception/FileNotFoundException.php | 2 +- Filesystem.php | 66 ++++++++++++++--------------- Path.php | 8 ++-- Tests/FilesystemTest.php | 4 +- Tests/FilesystemTestCase.php | 4 +- 5 files changed, 42 insertions(+), 42 deletions(-) diff --git a/Exception/FileNotFoundException.php b/Exception/FileNotFoundException.php index 06b732b16..d0d27977d 100644 --- a/Exception/FileNotFoundException.php +++ b/Exception/FileNotFoundException.php @@ -25,7 +25,7 @@ public function __construct(?string $message = null, int $code = 0, ?\Throwable if (null === $path) { $message = 'File could not be found.'; } else { - $message = sprintf('File "%s" could not be found.', $path); + $message = \sprintf('File "%s" could not be found.', $path); } } diff --git a/Filesystem.php b/Filesystem.php index 2cd09747c..00552c572 100644 --- a/Filesystem.php +++ b/Filesystem.php @@ -38,7 +38,7 @@ public function copy(string $originFile, string $targetFile, bool $overwriteNewe { $originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://'); if ($originIsLocal && !is_file($originFile)) { - throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile); + throw new FileNotFoundException(\sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile); } $this->mkdir(\dirname($targetFile)); @@ -51,12 +51,12 @@ public function copy(string $originFile, string $targetFile, bool $overwriteNewe if ($doCopy) { // https://bugs.php.net/64634 if (!$source = self::box('fopen', $originFile, 'r')) { - throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile); + throw new IOException(\sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile); } // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default if (!$target = self::box('fopen', $targetFile, 'w', false, stream_context_create(['ftp' => ['overwrite' => true]]))) { - throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile); + throw new IOException(\sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile); } $bytesCopied = stream_copy_to_stream($source, $target); @@ -65,7 +65,7 @@ public function copy(string $originFile, string $targetFile, bool $overwriteNewe unset($source, $target); if (!is_file($targetFile)) { - throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile); + throw new IOException(\sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile); } if ($originIsLocal) { @@ -76,7 +76,7 @@ public function copy(string $originFile, string $targetFile, bool $overwriteNewe self::box('touch', $targetFile, filemtime($originFile)); if ($bytesCopied !== $bytesOrigin = filesize($originFile)) { - throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile); + throw new IOException(\sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile); } } } @@ -95,7 +95,7 @@ public function mkdir(string|iterable $dirs, int $mode = 0777): void } if (!self::box('mkdir', $dir, $mode, true) && !is_dir($dir)) { - throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir); + throw new IOException(\sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir); } } } @@ -109,7 +109,7 @@ public function exists(string|iterable $files): bool foreach ($this->toIterable($files) as $file) { if (\strlen($file) > $maxPathLength) { - throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file); + throw new IOException(\sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file); } if (!file_exists($file)) { @@ -132,7 +132,7 @@ public function touch(string|iterable $files, ?int $time = null, ?int $atime = n { foreach ($this->toIterable($files) as $file) { if (!($time ? self::box('touch', $file, $time, $atime) : self::box('touch', $file))) { - throw new IOException(sprintf('Failed to touch "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(\sprintf('Failed to touch "%s": ', $file).self::$lastError, 0, null, $file); } } } @@ -160,7 +160,7 @@ private static function doRemove(array $files, bool $isRecursive): void if (is_link($file)) { // See https://bugs.php.net/52176 if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) { - throw new IOException(sprintf('Failed to remove symlink "%s": ', $file).self::$lastError); + throw new IOException(\sprintf('Failed to remove symlink "%s": ', $file).self::$lastError); } } elseif (is_dir($file)) { if (!$isRecursive) { @@ -191,10 +191,10 @@ private static function doRemove(array $files, bool $isRecursive): void $file = $origFile; } - throw new IOException(sprintf('Failed to remove directory "%s": ', $file).$lastError); + throw new IOException(\sprintf('Failed to remove directory "%s": ', $file).$lastError); } } elseif (!self::box('unlink', $file) && ((self::$lastError && str_contains(self::$lastError, 'Permission denied')) || file_exists($file))) { - throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError); + throw new IOException(\sprintf('Failed to remove file "%s": ', $file).self::$lastError); } } } @@ -212,7 +212,7 @@ public function chmod(string|iterable $files, int $mode, int $umask = 0000, bool { foreach ($this->toIterable($files) as $file) { if (!self::box('chmod', $file, $mode & ~$umask)) { - throw new IOException(sprintf('Failed to chmod file "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(\sprintf('Failed to chmod file "%s": ', $file).self::$lastError, 0, null, $file); } if ($recursive && is_dir($file) && !is_link($file)) { $this->chmod(new \FilesystemIterator($file), $mode, $umask, true); @@ -236,11 +236,11 @@ public function chown(string|iterable $files, string|int $user, bool $recursive } if (is_link($file) && \function_exists('lchown')) { if (!self::box('lchown', $file, $user)) { - throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(\sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file); } } else { if (!self::box('chown', $file, $user)) { - throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(\sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file); } } } @@ -262,11 +262,11 @@ public function chgrp(string|iterable $files, string|int $group, bool $recursive } if (is_link($file) && \function_exists('lchgrp')) { if (!self::box('lchgrp', $file, $group)) { - throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(\sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file); } } else { if (!self::box('chgrp', $file, $group)) { - throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(\sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file); } } } @@ -282,7 +282,7 @@ public function rename(string $origin, string $target, bool $overwrite = false): { // we check that target does not exist if (!$overwrite && $this->isReadable($target)) { - throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target); + throw new IOException(\sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target); } if (!self::box('rename', $origin, $target)) { @@ -293,7 +293,7 @@ public function rename(string $origin, string $target, bool $overwrite = false): return; } - throw new IOException(sprintf('Cannot rename "%s" to "%s": ', $origin, $target).self::$lastError, 0, null, $target); + throw new IOException(\sprintf('Cannot rename "%s" to "%s": ', $origin, $target).self::$lastError, 0, null, $target); } } @@ -307,7 +307,7 @@ private function isReadable(string $filename): bool $maxPathLength = \PHP_MAXPATHLEN - 2; if (\strlen($filename) > $maxPathLength) { - throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename); + throw new IOException(\sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename); } return is_readable($filename); @@ -364,7 +364,7 @@ public function hardlink(string $originFile, string|iterable $targetFiles): void } if (!is_file($originFile)) { - throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.', $originFile)); + throw new FileNotFoundException(\sprintf('Origin file "%s" is not a file.', $originFile)); } foreach ($this->toIterable($targetFiles) as $targetFile) { @@ -388,10 +388,10 @@ private function linkException(string $origin, string $target, string $linkType) { if (self::$lastError) { if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError, 'error code(1314)')) { - throw new IOException(sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target); + throw new IOException(\sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target); } } - throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": ', $linkType, $origin, $target).self::$lastError, 0, null, $target); + throw new IOException(\sprintf('Failed to create "%s" link from "%s" to "%s": ', $linkType, $origin, $target).self::$lastError, 0, null, $target); } /** @@ -428,11 +428,11 @@ public function readlink(string $path, bool $canonicalize = false): ?string public function makePathRelative(string $endPath, string $startPath): string { if (!$this->isAbsolutePath($startPath)) { - throw new InvalidArgumentException(sprintf('The start path "%s" is not absolute.', $startPath)); + throw new InvalidArgumentException(\sprintf('The start path "%s" is not absolute.', $startPath)); } if (!$this->isAbsolutePath($endPath)) { - throw new InvalidArgumentException(sprintf('The end path "%s" is not absolute.', $endPath)); + throw new InvalidArgumentException(\sprintf('The end path "%s" is not absolute.', $endPath)); } // Normalize separators on Windows @@ -518,7 +518,7 @@ public function mirror(string $originDir, string $targetDir, ?\Traversable $iter $originDirLen = \strlen($originDir); if (!$this->exists($originDir)) { - throw new IOException(sprintf('The origin directory specified "%s" was not found.', $originDir), 0, null, $originDir); + throw new IOException(\sprintf('The origin directory specified "%s" was not found.', $originDir), 0, null, $originDir); } // Iterate in destination folder to remove obsolete entries @@ -562,7 +562,7 @@ public function mirror(string $originDir, string $targetDir, ?\Traversable $iter } elseif (is_file($file)) { $this->copy($file, $target, $options['override'] ?? false); } else { - throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); + throw new IOException(\sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); } } } @@ -638,7 +638,7 @@ public function tempnam(string $dir, string $prefix, string $suffix = ''): strin public function dumpFile(string $filename, $content): void { if (\is_array($content)) { - throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); + throw new \TypeError(\sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); } $dir = \dirname($filename); @@ -659,7 +659,7 @@ public function dumpFile(string $filename, $content): void try { if (false === self::box('file_put_contents', $tmpFile, $content)) { - throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename); + throw new IOException(\sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename); } self::box('chmod', $tmpFile, self::box('fileperms', $filename) ?: 0666 & ~umask()); @@ -683,7 +683,7 @@ public function dumpFile(string $filename, $content): void public function appendToFile(string $filename, $content, bool $lock = false): void { if (\is_array($content)) { - throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); + throw new \TypeError(\sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); } $dir = \dirname($filename); @@ -693,7 +693,7 @@ public function appendToFile(string $filename, $content, bool $lock = false): vo } if (false === self::box('file_put_contents', $filename, $content, \FILE_APPEND | ($lock ? \LOCK_EX : 0))) { - throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename); + throw new IOException(\sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename); } } @@ -705,12 +705,12 @@ public function appendToFile(string $filename, $content, bool $lock = false): vo public function readFile(string $filename): string { if (is_dir($filename)) { - throw new IOException(sprintf('Failed to read file "%s": File is a directory.', $filename)); + throw new IOException(\sprintf('Failed to read file "%s": File is a directory.', $filename)); } $content = self::box('file_get_contents', $filename); if (false === $content) { - throw new IOException(sprintf('Failed to read file "%s": ', $filename).self::$lastError, 0, null, $filename); + throw new IOException(\sprintf('Failed to read file "%s": ', $filename).self::$lastError, 0, null, $filename); } return $content; @@ -734,7 +734,7 @@ private function getSchemeAndHierarchy(string $filename): array private static function assertFunctionExists(string $func): void { if (!\function_exists($func)) { - throw new IOException(sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.', $func)); + throw new IOException(\sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.', $func)); } } diff --git a/Path.php b/Path.php index db9ce4be9..2f2e87903 100644 --- a/Path.php +++ b/Path.php @@ -437,11 +437,11 @@ public static function isRelative(string $path): bool public static function makeAbsolute(string $path, string $basePath): string { if ('' === $basePath) { - throw new InvalidArgumentException(sprintf('The base path must be a non-empty string. Got: "%s".', $basePath)); + throw new InvalidArgumentException(\sprintf('The base path must be a non-empty string. Got: "%s".', $basePath)); } if (!self::isAbsolute($basePath)) { - throw new InvalidArgumentException(sprintf('The base path "%s" is not an absolute path.', $basePath)); + throw new InvalidArgumentException(\sprintf('The base path "%s" is not an absolute path.', $basePath)); } if (self::isAbsolute($path)) { @@ -531,12 +531,12 @@ public static function makeRelative(string $path, string $basePath): string // If the passed path is absolute, but the base path is not, we // cannot generate a relative path if ('' !== $root && '' === $baseRoot) { - throw new InvalidArgumentException(sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath)); + throw new InvalidArgumentException(\sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath)); } // Fail if the roots of the two paths are different if ($baseRoot && $root !== $baseRoot) { - throw new InvalidArgumentException(sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot)); + throw new InvalidArgumentException(\sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot)); } if ('' === $relativeBasePath) { diff --git a/Tests/FilesystemTest.php b/Tests/FilesystemTest.php index fe5a890d9..56fffbe46 100644 --- a/Tests/FilesystemTest.php +++ b/Tests/FilesystemTest.php @@ -1823,7 +1823,7 @@ public function testReadFile() public function testReadNonExistentFile() { $this->expectException(IOException::class); - $this->expectExceptionMessageMatches(sprintf('#^Failed to read file ".+%1$sTests/invalid"\\: file_get_contents\\(.+%1$sTests/invalid\\)\\: Failed to open stream\\: No such file or directory$#', preg_quote(\DIRECTORY_SEPARATOR))); + $this->expectExceptionMessageMatches(\sprintf('#^Failed to read file ".+%1$sTests/invalid"\\: file_get_contents\\(.+%1$sTests/invalid\\)\\: Failed to open stream\\: No such file or directory$#', preg_quote(\DIRECTORY_SEPARATOR))); $this->filesystem->readFile(__DIR__.'/invalid'); } @@ -1831,7 +1831,7 @@ public function testReadNonExistentFile() public function testReadDirectory() { $this->expectException(IOException::class); - $this->expectExceptionMessageMatches(sprintf('#^Failed to read file ".+%sTests"\\: File is a directory\\.$#', preg_quote(\DIRECTORY_SEPARATOR))); + $this->expectExceptionMessageMatches(\sprintf('#^Failed to read file ".+%sTests"\\: File is a directory\\.$#', preg_quote(\DIRECTORY_SEPARATOR))); $this->filesystem->readFile(__DIR__); } diff --git a/Tests/FilesystemTestCase.php b/Tests/FilesystemTestCase.php index 417d665cc..ce9176a47 100644 --- a/Tests/FilesystemTestCase.php +++ b/Tests/FilesystemTestCase.php @@ -82,11 +82,11 @@ protected function tearDown(): void */ protected function assertFilePermissions($expectedFilePerms, $filePath) { - $actualFilePerms = (int) substr(sprintf('%o', fileperms($filePath)), -3); + $actualFilePerms = (int) substr(\sprintf('%o', fileperms($filePath)), -3); $this->assertEquals( $expectedFilePerms, $actualFilePerms, - sprintf('File permissions for %s must be %s. Actual %s', $filePath, $expectedFilePerms, $actualFilePerms) + \sprintf('File permissions for %s must be %s. Actual %s', $filePath, $expectedFilePerms, $actualFilePerms) ); } From 454d22b4731b7da8a10f4f20839042c41df90c2c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 6 Jul 2024 09:57:16 +0200 Subject: [PATCH 12/15] Update .gitattributes --- .gitattributes | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 84c7add05..14c3c3594 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore From bea731a0b0ae9e51a44d83a445db09fb89952d3a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 6 Jul 2024 20:13:11 +0200 Subject: [PATCH 13/15] do not use uniqid() in tests updates the remaining tests that were not covered by #57665 --- Tests/FilesystemTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FilesystemTest.php b/Tests/FilesystemTest.php index 57939545c..d58b88a7b 100644 --- a/Tests/FilesystemTest.php +++ b/Tests/FilesystemTest.php @@ -863,7 +863,7 @@ public function testRenameOverwritesTheTargetIfItAlreadyExists() public function testRenameThrowsExceptionOnError() { $this->expectException(IOException::class); - $file = $this->workspace.\DIRECTORY_SEPARATOR.uniqid('fs_test_', true); + $file = $this->workspace.\DIRECTORY_SEPARATOR.'does-not-exist'; $newPath = $this->workspace.\DIRECTORY_SEPARATOR.'new_file'; $this->filesystem->rename($file, $newPath); From c46c178f375c2dfddc7b6a32731077c778e14264 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 23 Jul 2024 12:30:55 +0200 Subject: [PATCH 14/15] stop using uniqid() to create temporary files --- Filesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Filesystem.php b/Filesystem.php index 137cb4e99..45e9a4430 100644 --- a/Filesystem.php +++ b/Filesystem.php @@ -611,7 +611,7 @@ public function tempnam(string $dir, string $prefix, string $suffix = ''): strin // Loop until we create a valid temp file or have reached 10 attempts for ($i = 0; $i < 10; ++$i) { // Create a unique filename - $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true).$suffix; + $tmpFile = $dir.'/'.$prefix.bin2hex(random_bytes(4)).$suffix; // Use fopen instead of file_exists as some streams do not support stat // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability From b8871af1cde34a557c5f1375190dff02eada4c5a Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sun, 6 Oct 2024 18:45:39 +0200 Subject: [PATCH 15/15] CS: clean some whitespaces/indentation --- Filesystem.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Filesystem.php b/Filesystem.php index 769a01acb..6d896b0b9 100644 --- a/Filesystem.php +++ b/Filesystem.php @@ -224,6 +224,7 @@ public function chmod(string|iterable $files, int $mode, int $umask = 0000, bool * Change the owner of an array of files or directories. * * This method always throws on Windows, as the underlying PHP function is not supported. + * * @see https://www.php.net/chown * * @param string|int $user A user name or number @@ -253,6 +254,7 @@ public function chown(string|iterable $files, string|int $user, bool $recursive * Change the group of an array of files or directories. * * This method always throws on Windows, as the underlying PHP function is not supported. + * * @see https://www.php.net/chgrp * * @param string|int $group A group name or number