From b68583ca43731db0ea7f104b003b1ab0f8799aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 15 Nov 2025 13:46:23 +0100 Subject: [PATCH 1/7] feat: Introduce new tmp methods --- src/FS.php | 57 ++++++ src/FakeFileSystem.php | 13 ++ src/FileSystem.php | 43 +++++ src/NativeFileSystem.php | 45 +++++ src/ReadOnlyFileSystem.php | 17 ++ .../NativeFileSystem/NativeFileSystemTest.php | 168 ++++++++++++++++++ 6 files changed, 343 insertions(+) diff --git a/src/FS.php b/src/FS.php index 974ed61..ee46edf 100644 --- a/src/FS.php +++ b/src/FS.php @@ -253,6 +253,9 @@ public static function mirror(string $originDir, string $targetDir, ?Traversable } /** + * @deprecated Deprecated since 2.0. Use `Path::isAbsolute()` instead. + * @see Path::isAbsolute() + * * Returns whether the file path is an absolute path. */ public static function isAbsolutePath(string $file): bool @@ -299,6 +302,9 @@ public static function appendToFile(string $filename, $content/* , bool $lock = } /** + * @deprecated Deprecated since 2.0. Use `Path::isRelative()` instead. Will be removed in 3.0. + * @see Path::isRelative()) + * * Returns whether a path is relative. * * @param string $path a path string @@ -317,6 +323,9 @@ public static function escapePath(string $path): string } /** + * @deprecated Use the `::readFile()` method. Deprecated since 2.0 and it will be removed in 3.0. + * @see SymfonyFileSystem::readFile() + * * Gets the contents of a file. * * @param string $file File path @@ -341,6 +350,9 @@ public static function readFile(string $file): string } /** + * @deprecated Use the `::tmpDir()` method. Deprecated since 2.0 and it will be removed in 3.0. + * @see self::tmpDir() + * * Creates a temporary directory. * * @param string $namespace the directory path in the system's temporary directory @@ -354,6 +366,51 @@ public static function makeTmpDir(string $namespace, string $className): string } /** + * Creates a temporary file with support for custom stream wrappers. Same as tempnam(), + * but targets the system default temporary directory by default and has a more consistent + * name with tmpDir. + * + * @param string $prefix The prefix of the generated temporary file name. + * @param string $suffix The suffix of the generated temporary file name. + * @param string $targetDirectory The directory where to create the temporary directory. + * Defaults to the system default temporary directory. + * + * @throws IOException + * + * @return string The new temporary directory pathname. + * + * @see tempnam() + * @see SymfonyFileSystem::tempnam() + * @see self::tmpDir() + */ + public function tmpFile(string $prefix, string $suffix = '', ?string $targetDirectory = null): string + { + return self::getInstance()->tmpFile(...func_get_args()); + } + + /** + * Creates a temporary directory with support for custom stream wrappers. Similar to tempnam() + * but creates a directory instead of a file. + * + * @param string|null $prefix The prefix of the generated temporary directory name. + * @param string $targetDirectory The directory where to create the temporary directory. + * Defaults to the system default temporary directory. + * + * @throws IOException + * + * @return string The new temporary directory pathname. + * + * @see tempnam() + */ + public function tmpDir(string $prefix, ?string $targetDirectory = null): string + { + return self::getInstance()->tmpDir(...func_get_args()); + } + + /** + * @deprecated Deprecated since 2.0. Use `Path::isRelative()` instead. Will be removed in 3.0. + * Using a namespaced dir is an antipattern with parallel testing. + * * Gets a namespaced temporary directory. * * @param string $namespace the directory path in the system's temporary directory diff --git a/src/FakeFileSystem.php b/src/FakeFileSystem.php index 0c9b43c..a452a3a 100644 --- a/src/FakeFileSystem.php +++ b/src/FakeFileSystem.php @@ -199,4 +199,17 @@ public function normalizedRealPath(string $file): string { throw new DomainException('Unexpected call.'); } + + public function tmpFile( + string $prefix, + string $suffix = '', + ?string $targetDirectory = null, + ): string { + throw new DomainException('Unexpected call.'); + } + + public function tmpDir(string $prefix, ?string $targetDirectory = null): string + { + throw new DomainException('Unexpected call.'); + } } diff --git a/src/FileSystem.php b/src/FileSystem.php index 928249e..20a2a48 100644 --- a/src/FileSystem.php +++ b/src/FileSystem.php @@ -48,6 +48,7 @@ use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Finder\Finder; +use function tempnam; interface FileSystem extends SymfonyFileSystem { @@ -92,6 +93,9 @@ public function normalizedRealPath(string $file): string; public function getFileContents(string $file): string; /** + * @deprecated Use the `::tmpDir()` method. Deprecated since 2.0 and it will be removed in 3.0. + * @see self::tmpDir() + * * Creates a temporary directory. * * @param string $namespace the directory path in the system's temporary directory @@ -102,6 +106,45 @@ public function getFileContents(string $file): string; public function makeTmpDir(string $namespace, string $className): string; /** + * Creates a temporary file with support for custom stream wrappers. Same as tempnam(), + * but targets the system default temporary directory by default and has a more consistent + * name with tmpDir. + * + * @param string $prefix The prefix of the generated temporary file name. + * @param string $suffix The suffix of the generated temporary file name. + * @param string $targetDirectory The directory where to create the temporary directory. + * Defaults to the system default temporary directory. + * + * @throws IOException + * + * @return string The new temporary directory pathname. + * + * @see tempnam() + * @see SymfonyFileSystem::tempnam() + * @see self::tmpDir() + */ + public function tmpFile(string $prefix, string $suffix = '', ?string $targetDirectory = null): string; + + /** + * Creates a temporary directory with support for custom stream wrappers. Similar to tempnam() + * but creates a directory instead of a file. + * + * @param string|null $prefix The prefix of the generated temporary directory name. + * @param string $targetDirectory The directory where to create the temporary directory. + * Defaults to the system default temporary directory. + * + * @throws IOException + * + * @return string The new temporary directory pathname. + * + * @see tempnam() + */ + public function tmpDir(string $prefix, ?string $targetDirectory = null): string; + + /** + * @deprecated Deprecated since 2.0. Use `Path::isRelative()` instead. Will be removed in 3.0. + * Using a namespaced dir is an antipattern with parallel testing. + * * Gets a namespaced temporary directory. * * @param string $namespace the directory path in the system's temporary directory diff --git a/src/NativeFileSystem.php b/src/NativeFileSystem.php index 297e044..9077b44 100644 --- a/src/NativeFileSystem.php +++ b/src/NativeFileSystem.php @@ -51,10 +51,12 @@ use Symfony\Component\Filesystem\Path; use Symfony\Component\Finder\Finder; use Webmozart\Assert\Assert; +use function bin2hex; use function function_exists; use function is_dir; use function is_file; use function is_readable; +use function random_bytes; use function random_int; use function realpath; use function restore_error_handler; @@ -137,6 +139,49 @@ public function makeTmpDir(string $namespace, string $className): string return $tmpDir; } + public function tmpFile(string $prefix, string $suffix = '', ?string $targetDirectory = null): string + { + return $this->tempnam( + $targetDirectory ?? sys_get_temp_dir(), + $prefix, + $suffix, + ); + } + + public function tmpDir(string $prefix, ?string $targetDirectory = null): string + { + $targetDirectory ??= sys_get_temp_dir(); + + for ($i = 0; $i < 10; ++$i) { + // Create a unique directory name using the same pattern as Symfony's tempnam() + $tmpDir = $targetDirectory.DIRECTORY_SEPARATOR.$prefix.bin2hex(random_bytes(4)); + + // Might be better to us the Filesystem::box(); but its an internal + // API. + // It is probably not a big deal given the usage of this library but + // would need to be adjusted if merged to Symfony. + if ($this->exists($tmpDir)) { + continue; + } + + try { + $this->mkdir($tmpDir); + + return $tmpDir; + } catch (IOException) { + continue; + } + } + + throw new IOException( + sprintf( + 'A temporary directory could not be created in "%s": %s', + $targetDirectory, + self::$lastError, + ) + ); + } + public function getNamespacedTmpDir(string $namespace): string { // Usage of realpath() is important if the temporary directory is a diff --git a/src/ReadOnlyFileSystem.php b/src/ReadOnlyFileSystem.php index 9f326c1..de9ff7c 100644 --- a/src/ReadOnlyFileSystem.php +++ b/src/ReadOnlyFileSystem.php @@ -178,6 +178,23 @@ public function getNamespacedTmpDir(string $namespace): string return ''; } + public function tmpFile( + string $prefix, + string $suffix = '', + ?string $targetDirectory = null, + ): string { + $this->handleWrite(__METHOD__); + + return ''; + } + + public function tmpDir(string $prefix, ?string $targetDirectory = null): string + { + $this->handleWrite(__METHOD__); + + return ''; + } + private function handleWrite(string $methodName): void { if ($this->failOnWrite) { diff --git a/tests/NativeFileSystem/NativeFileSystemTest.php b/tests/NativeFileSystem/NativeFileSystemTest.php index 90f9a36..c4542e5 100644 --- a/tests/NativeFileSystem/NativeFileSystemTest.php +++ b/tests/NativeFileSystem/NativeFileSystemTest.php @@ -37,16 +37,24 @@ namespace Fidry\FileSystem\Tests\NativeFileSystem; use Fidry\FileSystem\FileSystem; +use Fidry\FileSystem\FS; use Fidry\FileSystem\NativeFileSystem; use Fidry\FileSystem\Test\FileSystemTestCase; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Path; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; +use function array_map; +use function iterator_to_array; use function realpath; +use function sort; use function str_replace; +use function sys_get_temp_dir; use const DIRECTORY_SEPARATOR; use const PHP_OS_FAMILY; +use const SORT_STRING; /** * @internal @@ -244,8 +252,168 @@ public static function normalizedRealPathProvider(): iterable ]; } + public function test_it_can_create_a_temporary_file_in_a_target_directory(): void + { + $targetDirectory = sys_get_temp_dir(); + $before = self::snapshotDirContent($targetDirectory); + + try { + $temporaryFile = $this->fileSystem->tempnam($targetDirectory, 'Prefix', 'Suffix'); + + $expected = [...$before, $temporaryFile]; + $actual = self::snapshotDirContent($targetDirectory); + + self::assertFileIsReadable($temporaryFile); + self::assertEqualsCanonicalizing($expected, $actual); + } finally { + FS::remove($temporaryFile); + } + + $fileName = str_replace( + $targetDirectory.DIRECTORY_SEPARATOR, + '', + $temporaryFile, + ); + + self::assertMatchesRegularExpression( + '/^Prefix[\p{L}\d]+Suffix$/', + $fileName, + ); + } + + public function test_it_can_create_a_temporary_file_in_a_target_directory_with_the_tmp_file_method(): void + { + $targetDirectory = $this->tmp; + $before = self::snapshotDirContent($targetDirectory); + + try { + $temporaryFile = $this->fileSystem->tmpFile('Prefix', 'Suffix', $targetDirectory); + + $expected = [...$before, $temporaryFile]; + $actual = self::snapshotDirContent($targetDirectory); + + self::assertFileIsReadable($temporaryFile); + self::assertEqualsCanonicalizing($expected, $actual); + } finally { + FS::remove($temporaryFile); + } + + $fileName = str_replace( + $targetDirectory.DIRECTORY_SEPARATOR, + '', + $temporaryFile, + ); + + self::assertMatchesRegularExpression( + '/^Prefix[\p{L}\d]+Suffix$/', + $fileName, + ); + } + + public function test_it_can_create_a_temporary_file_which_targets_the_system_default_tmp_dir_by_default(): void + { + $targetDirectory = sys_get_temp_dir(); + $before = self::snapshotDirContent($targetDirectory); + + try { + $temporaryFile = $this->fileSystem->tmpFile('Prefix', 'Suffix'); + + $expected = [...$before, $temporaryFile]; + $actual = self::snapshotDirContent($targetDirectory); + + self::assertFileIsReadable($temporaryFile); + self::assertEqualsCanonicalizing($expected, $actual); + } finally { + FS::remove($temporaryFile); + } + + $fileName = str_replace( + $targetDirectory.DIRECTORY_SEPARATOR, + '', + $temporaryFile, + ); + + self::assertMatchesRegularExpression( + '/^Prefix[\p{L}\d]+Suffix$/', + $fileName, + ); + } + + public function test_it_can_create_a_temporary_directory(): void + { + $targetDirectory = sys_get_temp_dir(); + $before = self::snapshotDirContent($targetDirectory); + + try { + $temporaryDirectory = $this->fileSystem->tmpDir('Prefix'); + + $expected = [...$before, $temporaryDirectory]; + $actual = self::snapshotDirContent($targetDirectory); + + self::assertDirectoryIsReadable($temporaryDirectory); + self::assertEqualsCanonicalizing($expected, $actual); + } finally { + FS::remove($temporaryDirectory); + } + + $directoryName = str_replace( + $targetDirectory.DIRECTORY_SEPARATOR, + '', + $temporaryDirectory, + ); + + self::assertMatchesRegularExpression( + '/^Prefix[\p{L}\d]+$/', + $directoryName, + ); + } + + public function test_it_can_create_a_temporary_directory_in_a_target_directory(): void + { + $targetDirectory = $this->tmp; + $before = self::snapshotDirContent($targetDirectory); + + try { + $temporaryDirectory = $this->fileSystem->tmpDir('Prefix', $targetDirectory); + + $expected = [...$before, $temporaryDirectory]; + $actual = self::snapshotDirContent($targetDirectory); + + self::assertDirectoryIsReadable($temporaryDirectory); + self::assertEqualsCanonicalizing($expected, $actual); + } finally { + FS::remove($temporaryDirectory); + } + + $directoryName = str_replace( + $targetDirectory.DIRECTORY_SEPARATOR, + '', + $temporaryDirectory, + ); + + self::assertMatchesRegularExpression( + '/^Prefix[\p{L}\d]+$/', + $directoryName, + ); + } + private static function isWindows(): bool { return PHP_OS_FAMILY === 'Windows'; } + + private static function snapshotDirContent(string $directory): array + { + $filesAndDirectories = Finder::create() + ->in($directory) + ->depth(0); + + $names = array_map( + static fn (SplFileInfo $fileInfo) => $fileInfo->getPathname(), + iterator_to_array($filesAndDirectories, preserve_keys: false), + ); + sort($names, SORT_STRING); + + return $names; + } } From f92d4dc3eae597ae33ca4a7b926ac2f59f31bae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 15 Nov 2025 13:54:55 +0100 Subject: [PATCH 2/7] docs --- README.md | 113 ++++++++++++++++++++++++++++++++++++++++++++- src/FS.php | 26 ++++++++++- src/FileSystem.php | 32 ++++++++++++- 3 files changed, 166 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5e8883d..15b621c 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,120 @@ This is a tiny wrapper around the [Symfony filesystem]. It provides a `FileSyste a few more utilities. +## New methods + +```php +interface FileSystem extends SymfonyFileSystem +{ + /** + * Replaces the path directory separator with the system one. + * + * For example, on Windows: + * 'C:/path/to/file' => 'C:\path\to\file', + */ + public function escapePath(string $path): string; + + /** + * Returns the absolute path, but the path will not be normalized. + * + * For example, `::realpath('C:\Users\Name\file.txt')` on Windows will + * return "C:\Users\Name\file.txt" (backslashes). + * + * @see https://php.net/manual/en/function.realpath.php + * + * @throws IOException When the file or symlink target does not exist. + */ + public function realPath(string $file): string; + + /** + * Returns the absolute normalized path. + * + * For example, `::realpath('C:\Users\Name\file.txt')` on Windows will + * return "C:/Users/Name/file.txt". + * + * @see https://php.net/manual/en/function.realpath.php + * + * @throws IOException When the file or symlink target does not exist. + */ + public function normalizedRealPath(string $file): string; + + /** + * Creates a temporary file with support for custom stream wrappers. Same as tempnam(), + * but targets the system default temporary directory by default and has a more consistent + * name with tmpDir. + * + * For example: + * + * ```php + * tmpFile('build') + * + * // on OSX + * => '/var/folders/p3/lkw0cgjj2fq0656q_9rd0mk80000gn/T/build8d9e0f1a' + * // on Windows + * => C:\Windows\Temp\build8d9e0f1a.tmp + * ``` + * + * @param string $prefix The prefix of the generated temporary file name. + * @param string $suffix The suffix of the generated temporary file name. + * @param string $targetDirectory The directory where to create the temporary directory. + * Defaults to the system default temporary directory. + * + * @return string The new temporary file pathname. + * + * @throws IOException + * + * @see tempnam() + * @see SymfonyFileSystem::tempnam() + * @see self::tmpDir() + */ + public function tmpFile(string $prefix, string $suffix = '', ?string $targetDirectory = null): string; + + /** + * Creates a temporary directory with support for custom stream wrappers. Similar to tempnam() + * but creates a directory instead of a file. + * + * For example: + * + * ```php + * tmpDir('build') + * + * // on OSX + * => '/var/folders/p3/lkw0cgjj2fq0656q_9rd0mk80000gn/T/build8d9e0f1a' + * // on Windows + * => C:\Windows\Temp\build8d9e0f1a.tmp + * ``` + * + * @param string|null $prefix The prefix of the generated temporary directory name. + * @param string $targetDirectory The directory where to create the temporary directory. + * Defaults to the system default temporary directory. + * + * @throws IOException + * + * @return string The new temporary directory pathname. + * + * @see tempnam() + */ + public function tmpDir(string $prefix, ?string $targetDirectory = null): string; + + /** + * Tells whether a file exists and is readable. + * + * @throws IOException When Window's path is longer than 258 characters + */ + public function isReadable(string $filename): bool; + + public function isReadableFile(string $filename): bool; + + public function isReadableDirectory(string $filename): bool; + + public function createFinder(): Finder; +} +``` + + ## FileSystemTestCase -An example of PHPUnit test: +An example of a PHPUnit test: ```php diff --git a/src/FS.php b/src/FS.php index ee46edf..0c0ec47 100644 --- a/src/FS.php +++ b/src/FS.php @@ -370,14 +370,25 @@ public static function makeTmpDir(string $namespace, string $className): string * but targets the system default temporary directory by default and has a more consistent * name with tmpDir. * + * For example: + * + * ```php + * tmpFile('build') + * + * // on OSX + * => '/var/folders/p3/lkw0cgjj2fq0656q_9rd0mk80000gn/T/build8d9e0f1a' + * // on Windows + * => C:\Windows\Temp\build8d9e0f1a.tmp + * ``` + * * @param string $prefix The prefix of the generated temporary file name. * @param string $suffix The suffix of the generated temporary file name. * @param string $targetDirectory The directory where to create the temporary directory. * Defaults to the system default temporary directory. * - * @throws IOException + * @return string The new temporary file pathname. * - * @return string The new temporary directory pathname. + * @throws IOException * * @see tempnam() * @see SymfonyFileSystem::tempnam() @@ -392,6 +403,17 @@ public function tmpFile(string $prefix, string $suffix = '', ?string $targetDire * Creates a temporary directory with support for custom stream wrappers. Similar to tempnam() * but creates a directory instead of a file. * + * For example: + * + * ```php + * tmpDir('build') + * + * // on OSX + * => '/var/folders/p3/lkw0cgjj2fq0656q_9rd0mk80000gn/T/build8d9e0f1a' + * // on Windows + * => C:\Windows\Temp\build8d9e0f1a.tmp + * ``` + * * @param string|null $prefix The prefix of the generated temporary directory name. * @param string $targetDirectory The directory where to create the temporary directory. * Defaults to the system default temporary directory. diff --git a/src/FileSystem.php b/src/FileSystem.php index 20a2a48..a1344f6 100644 --- a/src/FileSystem.php +++ b/src/FileSystem.php @@ -58,6 +58,12 @@ interface FileSystem extends SymfonyFileSystem */ public function isRelativePath(string $path): bool; + /** + * Replaces the path directory separator with the system one. + * + * For example, on Windows: + * 'C:/path/to/file' => 'C:\path\to\file', + */ public function escapePath(string $path): string; /** @@ -110,14 +116,25 @@ public function makeTmpDir(string $namespace, string $className): string; * but targets the system default temporary directory by default and has a more consistent * name with tmpDir. * + * For example: + * + * ```php + * tmpFile('build') + * + * // on OSX + * => '/var/folders/p3/lkw0cgjj2fq0656q_9rd0mk80000gn/T/build8d9e0f1a' + * // on Windows + * => C:\Windows\Temp\build8d9e0f1a.tmp + * ``` + * * @param string $prefix The prefix of the generated temporary file name. * @param string $suffix The suffix of the generated temporary file name. * @param string $targetDirectory The directory where to create the temporary directory. * Defaults to the system default temporary directory. * - * @throws IOException + * @return string The new temporary file pathname. * - * @return string The new temporary directory pathname. + * @throws IOException * * @see tempnam() * @see SymfonyFileSystem::tempnam() @@ -129,6 +146,17 @@ public function tmpFile(string $prefix, string $suffix = '', ?string $targetDire * Creates a temporary directory with support for custom stream wrappers. Similar to tempnam() * but creates a directory instead of a file. * + * For example: + * + * ```php + * tmpDir('build') + * + * // on OSX + * => '/var/folders/p3/lkw0cgjj2fq0656q_9rd0mk80000gn/T/build8d9e0f1a' + * // on Windows + * => C:\Windows\Temp\build8d9e0f1a.tmp + * ``` + * * @param string|null $prefix The prefix of the generated temporary directory name. * @param string $targetDirectory The directory where to create the temporary directory. * Defaults to the system default temporary directory. From e04ca1ad0191799161eb13ba3c67bed3e7736119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 15 Nov 2025 14:05:48 +0100 Subject: [PATCH 3/7] fix --- infection.json.dist | 2 ++ src/FS.php | 3 +-- src/FileSystem.php | 3 +-- tests/NativeFileSystem/NativeFileSystemTest.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/infection.json.dist b/infection.json.dist index 850280d..0a7e046 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -18,6 +18,8 @@ // Too dangerous to mutate outside a container "Fidry\\FileSystem\\NativeFileSystem::makeTmpDir", "Fidry\\FileSystem\\NativeFileSystem::getNamespacedTmpDir", + "Fidry\\FileSystem\\NativeFileSystem::tmpDir", + "Fidry\\FileSystem\\NativeFileSystem::tmpFile", "Fidry\\FileSystem\\Test\\FileSystemTestCase::tearDown" ], diff --git a/src/FS.php b/src/FS.php index 0c0ec47..a123604 100644 --- a/src/FS.php +++ b/src/FS.php @@ -386,9 +386,8 @@ public static function makeTmpDir(string $namespace, string $className): string * @param string $targetDirectory The directory where to create the temporary directory. * Defaults to the system default temporary directory. * - * @return string The new temporary file pathname. - * * @throws IOException + * @return string The new temporary file pathname. * * @see tempnam() * @see SymfonyFileSystem::tempnam() diff --git a/src/FileSystem.php b/src/FileSystem.php index a1344f6..c26744a 100644 --- a/src/FileSystem.php +++ b/src/FileSystem.php @@ -132,9 +132,8 @@ public function makeTmpDir(string $namespace, string $className): string; * @param string $targetDirectory The directory where to create the temporary directory. * Defaults to the system default temporary directory. * - * @return string The new temporary file pathname. - * * @throws IOException + * @return string The new temporary file pathname. * * @see tempnam() * @see SymfonyFileSystem::tempnam() diff --git a/tests/NativeFileSystem/NativeFileSystemTest.php b/tests/NativeFileSystem/NativeFileSystemTest.php index c4542e5..47e514c 100644 --- a/tests/NativeFileSystem/NativeFileSystemTest.php +++ b/tests/NativeFileSystem/NativeFileSystemTest.php @@ -276,7 +276,7 @@ public function test_it_can_create_a_temporary_file_in_a_target_directory(): voi ); self::assertMatchesRegularExpression( - '/^Prefix[\p{L}\d]+Suffix$/', + '/^Prefix[\p{L}\d\.]+Suffix$/', $fileName, ); } From 31ee7a87760a7701291dd90da4ca86afd73b191b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 15 Nov 2025 14:08:31 +0100 Subject: [PATCH 4/7] fix --- tests/NativeFileSystem/NativeFileSystemTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/NativeFileSystem/NativeFileSystemTest.php b/tests/NativeFileSystem/NativeFileSystemTest.php index 47e514c..87bbe02 100644 --- a/tests/NativeFileSystem/NativeFileSystemTest.php +++ b/tests/NativeFileSystem/NativeFileSystemTest.php @@ -305,7 +305,7 @@ public function test_it_can_create_a_temporary_file_in_a_target_directory_with_t ); self::assertMatchesRegularExpression( - '/^Prefix[\p{L}\d]+Suffix$/', + '/^Prefix[\p{L}\d\.]+Suffix$/', $fileName, ); } @@ -334,7 +334,7 @@ public function test_it_can_create_a_temporary_file_which_targets_the_system_def ); self::assertMatchesRegularExpression( - '/^Prefix[\p{L}\d]+Suffix$/', + '/^Prefix[\p{L}\d\.]+Suffix$/', $fileName, ); } From 5f647753a771112a970a51f33a5dbcd433f128fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 15 Nov 2025 14:17:02 +0100 Subject: [PATCH 5/7] fix? --- tests/NativeFileSystem/NativeFileSystemTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/NativeFileSystem/NativeFileSystemTest.php b/tests/NativeFileSystem/NativeFileSystemTest.php index 87bbe02..bbd2d8a 100644 --- a/tests/NativeFileSystem/NativeFileSystemTest.php +++ b/tests/NativeFileSystem/NativeFileSystemTest.php @@ -260,7 +260,7 @@ public function test_it_can_create_a_temporary_file_in_a_target_directory(): voi try { $temporaryFile = $this->fileSystem->tempnam($targetDirectory, 'Prefix', 'Suffix'); - $expected = [...$before, $temporaryFile]; + $expected = [...$before, Path::normalize($temporaryFile)]; $actual = self::snapshotDirContent($targetDirectory); self::assertFileIsReadable($temporaryFile); @@ -289,7 +289,7 @@ public function test_it_can_create_a_temporary_file_in_a_target_directory_with_t try { $temporaryFile = $this->fileSystem->tmpFile('Prefix', 'Suffix', $targetDirectory); - $expected = [...$before, $temporaryFile]; + $expected = [...$before, Path::normalize($temporaryFile)]; $actual = self::snapshotDirContent($targetDirectory); self::assertFileIsReadable($temporaryFile); @@ -318,7 +318,7 @@ public function test_it_can_create_a_temporary_file_which_targets_the_system_def try { $temporaryFile = $this->fileSystem->tmpFile('Prefix', 'Suffix'); - $expected = [...$before, $temporaryFile]; + $expected = [...$before, Path::normalize($temporaryFile)]; $actual = self::snapshotDirContent($targetDirectory); self::assertFileIsReadable($temporaryFile); From 749fe871732fcf78e891ce62e15839c15ff8709e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 15 Nov 2025 14:23:39 +0100 Subject: [PATCH 6/7] escape instead of normalize --- tests/NativeFileSystem/NativeFileSystemTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/NativeFileSystem/NativeFileSystemTest.php b/tests/NativeFileSystem/NativeFileSystemTest.php index bbd2d8a..bc493c7 100644 --- a/tests/NativeFileSystem/NativeFileSystemTest.php +++ b/tests/NativeFileSystem/NativeFileSystemTest.php @@ -260,7 +260,7 @@ public function test_it_can_create_a_temporary_file_in_a_target_directory(): voi try { $temporaryFile = $this->fileSystem->tempnam($targetDirectory, 'Prefix', 'Suffix'); - $expected = [...$before, Path::normalize($temporaryFile)]; + $expected = [...$before, FS::escapePath($temporaryFile)]; $actual = self::snapshotDirContent($targetDirectory); self::assertFileIsReadable($temporaryFile); @@ -289,7 +289,7 @@ public function test_it_can_create_a_temporary_file_in_a_target_directory_with_t try { $temporaryFile = $this->fileSystem->tmpFile('Prefix', 'Suffix', $targetDirectory); - $expected = [...$before, Path::normalize($temporaryFile)]; + $expected = [...$before, FS::escapePath($temporaryFile)]; $actual = self::snapshotDirContent($targetDirectory); self::assertFileIsReadable($temporaryFile); @@ -318,7 +318,7 @@ public function test_it_can_create_a_temporary_file_which_targets_the_system_def try { $temporaryFile = $this->fileSystem->tmpFile('Prefix', 'Suffix'); - $expected = [...$before, Path::normalize($temporaryFile)]; + $expected = [...$before, FS::escapePath($temporaryFile)]; $actual = self::snapshotDirContent($targetDirectory); self::assertFileIsReadable($temporaryFile); From 2ac7767fce5035bc36b48ebb22e32462835e138e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sat, 15 Nov 2025 14:28:21 +0100 Subject: [PATCH 7/7] fix --- src/NativeFileSystem.php | 10 ++++++---- tests/NativeFileSystem/NativeFileSystemTest.php | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/NativeFileSystem.php b/src/NativeFileSystem.php index 9077b44..6f6c48b 100644 --- a/src/NativeFileSystem.php +++ b/src/NativeFileSystem.php @@ -141,10 +141,12 @@ public function makeTmpDir(string $namespace, string $className): string public function tmpFile(string $prefix, string $suffix = '', ?string $targetDirectory = null): string { - return $this->tempnam( - $targetDirectory ?? sys_get_temp_dir(), - $prefix, - $suffix, + return $this->escapePath( + $this->tempnam( + $targetDirectory ?? sys_get_temp_dir(), + $prefix, + $suffix, + ), ); } diff --git a/tests/NativeFileSystem/NativeFileSystemTest.php b/tests/NativeFileSystem/NativeFileSystemTest.php index bc493c7..46f4c1d 100644 --- a/tests/NativeFileSystem/NativeFileSystemTest.php +++ b/tests/NativeFileSystem/NativeFileSystemTest.php @@ -270,7 +270,7 @@ public function test_it_can_create_a_temporary_file_in_a_target_directory(): voi } $fileName = str_replace( - $targetDirectory.DIRECTORY_SEPARATOR, + $targetDirectory.'/', // the incorrect directory separator on Windows is a bug in Symfony. '', $temporaryFile, );