From af75dac5f6fe49907c001091ff85cfc00a46ac20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Sun, 27 Sep 2026 16:03:30 +0200 Subject: [PATCH 1/3] [TASK] Guard package name type for version option PHPStan reports a possibly invalid array key type for the package name read from a fixture package `composer.json`, which is `mixed` at that point. The name is now verified to be a string before it is used to look up a version from the repository options. A non-string name fails package loading later on anyway, so behaviour does not change. --- src/Composer/Repository/FixturePathRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Repository/FixturePathRepository.php b/src/Composer/Repository/FixturePathRepository.php index 6154a8c..67d0089 100644 --- a/src/Composer/Repository/FixturePathRepository.php +++ b/src/Composer/Repository/FixturePathRepository.php @@ -172,7 +172,7 @@ protected function initialize(): void // copy symlink/relative options to transport options $package['transport-options'] = array_intersect_key($this->options, ['symlink' => true, 'relative' => true]); // use the version provided as option if available - if (isset($package['name'], $this->options['versions'][$package['name']])) { + if (isset($package['name']) && is_string($package['name']) && isset($this->options['versions'][$package['name']])) { $package['version'] = $this->options['versions'][$package['name']]; } From a1e1314d778bdab5a258231d0c670ef3fb94578b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Sun, 27 Sep 2026 16:03:30 +0200 Subject: [PATCH 2/3] [BUGFIX] Return processed paths from plugin config `Config::processFixtureExtensionPaths()` computed the processed path list, but returned the unprocessed input. `paths()`, `get()` and `all()` therefore handed out the configured keys as written in `composer.json`, regardless of the passed flag. The default mode, which should return absolute paths, was completely broken and a trailing slash reached the `fixture-path` repository url. The processed list is returned now. Relative mode normalizes the configured path using the composer filesystem utility, the same way the absolute mode already did. An empty result is kept as the current directory, and selections of different notations of the same path are merged instead of being dropped. Additionally: * `realpath()` normalizes before checking for an empty path, so `./` resolves to the base dir instead of reading an offset of an empty string. * `isRelativePathsFlag()` checks the flag bit with `!== 0`, which does not depend on the constant value. * An unused config instance left over in `handleRootPackageExtraConfig()` is removed. Resolves: #18 --- src/Plugin/Config.php | 23 +++-- tests/Unit/Plugin/ConfigTest.php | 163 +++++++++++++++++++++++++++++-- 2 files changed, 173 insertions(+), 13 deletions(-) diff --git a/src/Plugin/Config.php b/src/Plugin/Config.php index cc235c0..7e8eccb 100644 --- a/src/Plugin/Config.php +++ b/src/Plugin/Config.php @@ -142,10 +142,21 @@ protected function processFixtureExtensionPaths(array $paths, int $flags = 0): a $relativePaths = $this->isRelativePathsFlag($flags); $returnPaths = []; foreach ($paths as $path => $selection) { - $path = rtrim(($relativePaths ? $path : $this->realpath($path)), '/\\'); - $returnPaths[$path] = $selection; + if ($relativePaths) { + $path = $this->normalizePath((string)$path); + if ($path === '') { + // Keep the current directory, an empty url is not a valid fixture-path repository url. + $path = '.'; + } + } else { + $path = $this->realpath((string)$path); + } + // Different notations of the same path must not drop the selection of the former ones. + $returnPaths[$path] = isset($returnPaths[$path]) + ? array_values(array_unique(array_merge($returnPaths[$path], $selection))) + : $selection; } - return $paths; + return $returnPaths; } /** @@ -155,7 +166,7 @@ protected function processFixtureExtensionPaths(array $paths, int $flags = 0): a */ private function isRelativePathsFlag(int $flags): bool { - return ($flags & self::FLAG_PATHS_RELATIVE) === 1; + return ($flags & self::FLAG_PATHS_RELATIVE) !== 0; } /** @@ -165,10 +176,10 @@ private function isRelativePathsFlag(int $flags): bool */ private function realpath(string $path): string { + $path = $this->normalizePath($path); if ($path === '') { return $this->baseDir; } - $path = $this->normalizePath($path); if ($path[0] === '/' || (!empty($path[1]) && $path[1] === ':')) { return $path; } @@ -224,8 +235,6 @@ private static function handleRootPackageExtraConfig(IOInterface $io, RootPackag if (empty($fixtureExtensionPaths)) { return $rootPackageExtraConfig; } - $basePath = '/fake/root'; - $config = new self($basePath); $validPaths = []; foreach ($fixtureExtensionPaths as $path => $selection) { if (!is_array($selection)) { diff --git a/tests/Unit/Plugin/ConfigTest.php b/tests/Unit/Plugin/ConfigTest.php index 29e47b7..abb2997 100644 --- a/tests/Unit/Plugin/ConfigTest.php +++ b/tests/Unit/Plugin/ConfigTest.php @@ -116,6 +116,16 @@ public static function realpathDataSets(): \Generator 'path' => '/', 'expectedPath' => '/', ]; + yield 'Current directory path returns baseDir' => [ + 'baseDir' => '/fake/root', + 'path' => './', + 'expectedPath' => '/fake/root', + ]; + yield 'Relative path is prefixed with baseDir and trimmed' => [ + 'baseDir' => '/fake/root', + 'path' => 'some/relative/path/', + 'expectedPath' => '/fake/root/some/relative/path', + ]; } /** @@ -193,7 +203,8 @@ public static function loadCreatesConfigWithExpectedPathsDataSets(): \Generator { yield 'Empty extra config returns empty array' => [ 'extraConfig' => [], - 'expectedPaths' => [], + 'expectedRelativePaths' => [], + 'expectedAbsolutePaths' => [], 'expectedOutput' => '', ]; yield 'Other extra configuration are kept' => [ @@ -202,7 +213,8 @@ public static function loadCreatesConfigWithExpectedPathsDataSets(): \Generator 'extension-key' => 'some_extension_key', ], ], - 'expectedPaths' => [], + 'expectedRelativePaths' => [], + 'expectedAbsolutePaths' => [], 'expectedOutput' => '', ]; yield 'Non-array extra->sbuerk/fixture-packages/paths value is removed' => [ @@ -211,7 +223,8 @@ public static function loadCreatesConfigWithExpectedPathsDataSets(): \Generator 'paths' => false, ], ], - 'expectedPaths' => [], + 'expectedRelativePaths' => [], + 'expectedAbsolutePaths' => [], 'expectedOutput' => 'extra->sbuerk/fixture-packages/paths must be an array, "boolean" given.' . PHP_EOL, ]; yield 'Non-array extra->sbuerk/fixture-packages/paths value is removed keeping other settings' => [ @@ -221,16 +234,148 @@ public static function loadCreatesConfigWithExpectedPathsDataSets(): \Generator 'paths' => false, ], ], - 'expectedPaths' => [], + 'expectedRelativePaths' => [], + 'expectedAbsolutePaths' => [], 'expectedOutput' => 'extra->sbuerk/fixture-packages/paths must be an array, "boolean" given.' . PHP_EOL, ]; + yield 'Path without trailing slash' => [ + 'extraConfig' => [ + 'sbuerk/fixture-packages' => [ + 'paths' => [ + 'Tests/Functional/Fixtures/Extensions/*' => ['autoload'], + ], + ], + ], + 'expectedRelativePaths' => [ + 'Tests/Functional/Fixtures/Extensions/*' => ['autoload'], + ], + 'expectedAbsolutePaths' => [ + '/fake/root/Tests/Functional/Fixtures/Extensions/*' => ['autoload'], + ], + 'expectedOutput' => '', + ]; + yield 'Path with trailing slash is trimmed' => [ + 'extraConfig' => [ + 'sbuerk/fixture-packages' => [ + 'paths' => [ + 'Tests/Functional/Fixtures/Extensions/' => ['autoload'], + ], + ], + ], + 'expectedRelativePaths' => [ + 'Tests/Functional/Fixtures/Extensions' => ['autoload'], + ], + 'expectedAbsolutePaths' => [ + '/fake/root/Tests/Functional/Fixtures/Extensions' => ['autoload'], + ], + 'expectedOutput' => '', + ]; + yield 'Path with trailing backslash is trimmed' => [ + 'extraConfig' => [ + 'sbuerk/fixture-packages' => [ + 'paths' => [ + 'Tests\\Functional\\Fixtures\\Extensions\\' => ['autoload-dev'], + ], + ], + ], + 'expectedRelativePaths' => [ + 'Tests/Functional/Fixtures/Extensions' => ['autoload-dev'], + ], + 'expectedAbsolutePaths' => [ + '/fake/root/Tests/Functional/Fixtures/Extensions' => ['autoload-dev'], + ], + 'expectedOutput' => '', + ]; + yield 'Path list syntax with trailing slash is trimmed' => [ + 'extraConfig' => [ + 'sbuerk/fixture-packages' => [ + 'paths' => [ + 'Tests/Functional/Fixtures/Extensions/*/', + ], + ], + ], + 'expectedRelativePaths' => [ + 'Tests/Functional/Fixtures/Extensions/*' => ['autoload'], + ], + 'expectedAbsolutePaths' => [ + '/fake/root/Tests/Functional/Fixtures/Extensions/*' => ['autoload'], + ], + 'expectedOutput' => '', + ]; + yield 'Path outside project is kept relative or resolved against base dir' => [ + 'extraConfig' => [ + 'sbuerk/fixture-packages' => [ + 'paths' => [ + '../other-project/Fixtures/*/' => ['autoload', 'autoload-dev'], + ], + ], + ], + 'expectedRelativePaths' => [ + '../other-project/Fixtures/*' => ['autoload', 'autoload-dev'], + ], + 'expectedAbsolutePaths' => [ + '/fake/root/../other-project/Fixtures/*' => ['autoload', 'autoload-dev'], + ], + 'expectedOutput' => '', + ]; + yield 'Current directory path is kept as current directory or resolved to base dir' => [ + 'extraConfig' => [ + 'sbuerk/fixture-packages' => [ + 'paths' => [ + './' => ['autoload'], + ], + ], + ], + 'expectedRelativePaths' => [ + '.' => ['autoload'], + ], + 'expectedAbsolutePaths' => [ + '/fake/root' => ['autoload'], + ], + 'expectedOutput' => '', + ]; + yield 'Selections of paths normalized to the same path are merged' => [ + 'extraConfig' => [ + 'sbuerk/fixture-packages' => [ + 'paths' => [ + 'Tests/Fixtures/' => ['autoload'], + 'Tests/Fixtures' => ['autoload-dev', 'autoload'], + '/fake/root/Tests/Fixtures' => ['autoload-dev'], + ], + ], + ], + 'expectedRelativePaths' => [ + 'Tests/Fixtures' => ['autoload', 'autoload-dev'], + '/fake/root/Tests/Fixtures' => ['autoload-dev'], + ], + 'expectedAbsolutePaths' => [ + '/fake/root/Tests/Fixtures' => ['autoload', 'autoload-dev'], + ], + 'expectedOutput' => '', + ]; + yield 'Absolute path is kept absolute and trimmed' => [ + 'extraConfig' => [ + 'sbuerk/fixture-packages' => [ + 'paths' => [ + '/some/absolute/Fixtures/' => ['autoload'], + ], + ], + ], + 'expectedRelativePaths' => [ + '/some/absolute/Fixtures' => ['autoload'], + ], + 'expectedAbsolutePaths' => [ + '/some/absolute/Fixtures' => ['autoload'], + ], + 'expectedOutput' => '', + ]; } /** * @dataProvider loadCreatesConfigWithExpectedPathsDataSets * @test */ - public function loadCreatesConfigWithExpectedPaths(array $extraConfig, array $expectedPaths, string $expectedOutput): void + public function loadCreatesConfigWithExpectedPaths(array $extraConfig, array $expectedRelativePaths, array $expectedAbsolutePaths, string $expectedOutput): void { $rootPackage = new RootPackage('fake/package', '1.0.0', '1.0.0.0'); $rootPackage->setExtra($extraConfig); @@ -241,7 +386,13 @@ public function loadCreatesConfigWithExpectedPaths(array $extraConfig, array $ex $bufferedIO = new BufferIO(); $config = Config::load($composer, $bufferedIO); self::assertInstanceOf(Config::class, $config); - self::assertSame($expectedPaths, $config->paths(Config::FLAG_PATHS_RELATIVE)); + self::assertSame($expectedRelativePaths, $config->paths(Config::FLAG_PATHS_RELATIVE)); + self::assertSame($expectedRelativePaths, $config->get('paths', Config::FLAG_PATHS_RELATIVE)); + self::assertSame(['config' => ['paths' => $expectedRelativePaths]], $config->all(Config::FLAG_PATHS_RELATIVE)); + self::assertSame($expectedAbsolutePaths, $config->paths()); + self::assertSame($expectedAbsolutePaths, $config->paths(Config::FLAG_PATHS_DEFAULT)); + self::assertSame($expectedAbsolutePaths, $config->get('paths')); + self::assertSame(['config' => ['paths' => $expectedAbsolutePaths]], $config->all()); self::assertSame($expectedOutput, $bufferedIO->getOutput()); } } From d1388de9fd2ba1ee7ca7138a4e8881055807263e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Sun, 27 Sep 2026 16:07:57 +0200 Subject: [PATCH 3/3] [TASK] Raise phpunit to a version without advisory `phpunit/phpunit` was pinned to 9.6.22, which is affected by CVE-2026-24765 (PKSA-z3gr-8qht-p93v). Composer blocks packages with known security advisories by default in recent versions, which lets dependency installation fail in CI for PHP 8.1 and newer. The constraint is raised to `^9.6.33`, the first 9.6 release that is not affected, still supporting PHP 7.4. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 25d4a32..256d351 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "phpstan/phpstan": "^2.1.2", "phpstan/phpdoc-parser": "^1.30.1", "bnf/phpstan-psr-container": "^1.1.0", - "phpunit/phpunit": "9.6.22", + "phpunit/phpunit": "^9.6.33", "phpstan/phpstan-phpunit": "^2.0.4", "phpstan/phpstan-symfony": "^2.0.2", "phpstan/phpstan-deprecation-rules": "^2.0.1"