From 4dd4fc28fd70ec1ac64e292e508a1bb357c9d42a Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Mon, 31 Aug 2026 22:17:41 +0200 Subject: [PATCH] [BUGFIX] Exclude directories whose name contains a slash The packaging filter interpolates every entry of the exclude configuration straight into a slash-delimited regular expression, so an entry naming a nested directory such as `Resources/Private/Build` ends up as `/^Resources/Private/Build/i`. PHP reads that as the pattern `^Resources` followed by the modifiers `Private...`, emits "Unknown modifier 'P'" once per inspected file and returns false - so the directory is packaged instead of skipped, and the published archive carries the very sources the configuration excludes. Quote the configured entries. They are documented as directory and file names and the shipped default configuration contains no regular expressions, so nothing that works today changes: the file rule still matches a suffix, the directory rule still matches a prefix. The regression test fails against the old filter with exactly the file that should have been excluded. Assisted-by: claude-code:claude-opus-5 Agent-Session: https://claude.ai/code/session_01JYQciiXoiApXBfcJrFMnA9 Agent-Host: 32116e Signed-off-by: Sebastian Mendel --- src/Service/VersionService.php | 4 +- .../config_nested_directory.php | 8 ++ tests/Unit/Service/VersionServiceTest.php | 78 +++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Fixtures/ExcludeFromPackaging/config_nested_directory.php diff --git a/src/Service/VersionService.php b/src/Service/VersionService.php index b696709..34dad5d 100644 --- a/src/Service/VersionService.php +++ b/src/Service/VersionService.php @@ -81,7 +81,7 @@ public function createZipArchiveFromPath(string $path): string if ($current->isDir()) { // if $current is a directory, check for excluded directories foreach ($this->excludeConfiguration['directories'] as $excludeDirectory) { - if (preg_match('/^' . $excludeDirectory . '/i', $path)) { + if (preg_match('/^' . preg_quote((string)$excludeDirectory, '/') . '/i', $path)) { return false; } } @@ -90,7 +90,7 @@ public function createZipArchiveFromPath(string $path): string if ($current->isFile()) { // if $current is a file, check for excluded files foreach ($this->excludeConfiguration['files'] as $excludeFile) { - if (preg_match('/' . $excludeFile . '$/i', $filename)) { + if (preg_match('/' . preg_quote((string)$excludeFile, '/') . '$/i', $filename)) { return false; } } diff --git a/tests/Unit/Fixtures/ExcludeFromPackaging/config_nested_directory.php b/tests/Unit/Fixtures/ExcludeFromPackaging/config_nested_directory.php new file mode 100644 index 0000000..c178dc3 --- /dev/null +++ b/tests/Unit/Fixtures/ExcludeFromPackaging/config_nested_directory.php @@ -0,0 +1,8 @@ + [ + 'Resources/Private/Build', + ], + 'files' => [], +]; diff --git a/tests/Unit/Service/VersionServiceTest.php b/tests/Unit/Service/VersionServiceTest.php index abf4cf5..ff21ce5 100644 --- a/tests/Unit/Service/VersionServiceTest.php +++ b/tests/Unit/Service/VersionServiceTest.php @@ -19,6 +19,9 @@ class VersionServiceTest extends TestCase { + /** @var list */ + protected array $temporaryDirectories = []; + #[Test] public function defaultExcludeFromPackagingConfigurationIsUsedOnNonExistingEnvVar(): void { @@ -105,6 +108,81 @@ public function getVersionFilenameAsMd5Test(): void ); } + #[Test] + public function excludedDirectoryContainingASlashIsNotPackaged(): void + { + unset($_ENV); + putenv('TYPO3_EXCLUDE_FROM_PACKAGING=' . __DIR__ . '/../Fixtures/ExcludeFromPackaging/config_nested_directory.php'); + + $extensionPath = $this->createExtensionDirectory(); + $transactionPath = $this->createTemporaryDirectory(); + + $archivePath = (new VersionService('1.0.0', 'my_ext', $transactionPath)) + ->createZipArchiveFromPath($extensionPath); + + $archive = new \ZipArchive(); + $archive->open($archivePath); + $packagedFiles = []; + + for ($index = 0; $index < $archive->numFiles; $index++) { + $packagedFiles[] = $archive->getNameIndex($index); + } + + $archive->close(); + + self::assertContains('ext_emconf.php', $packagedFiles); + self::assertNotContains('Resources/Private/Build/gulpfile.js', $packagedFiles); + } + + /** + * Create an extension directory holding a valid ext_emconf.php and a file + * inside the nested directory the exclude configuration names. + */ + protected function createExtensionDirectory(): string + { + $path = $this->createTemporaryDirectory(); + + file_put_contents( + $path . '/ext_emconf.php', + ' \'1.0.0\', \'constraints\' => [\'depends\' => [\'typo3\' => \'13.4.0-13.4.99\']]];' . PHP_EOL + ); + + mkdir($path . '/Resources/Private/Build', 0777, true); + file_put_contents($path . '/Resources/Private/Build/gulpfile.js', '// build only'); + + return $path; + } + + protected function createTemporaryDirectory(): string + { + $path = sys_get_temp_dir() . '/tailor-test-' . bin2hex(random_bytes(8)); + mkdir($path, 0777, true); + $this->temporaryDirectories[] = $path; + + return $path; + } + + protected function tearDown(): void + { + foreach ($this->temporaryDirectories as $directory) { + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($files as $file) { + $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); + } + + rmdir($directory); + } + + $this->temporaryDirectories = []; + + parent::tearDown(); + } + /** * Invoke a protected / private method from VersionService *