diff --git a/src/Helper.php b/src/Helper.php index be41619..c3b1ce1 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -92,7 +92,7 @@ public static function removePharPrefix(string $path): string public static function normalizePath(string $path): string { return rtrim( - static::getCanonicalPath( + static::normalizeWindowsPath( static::removePharPrefix($path) ), '/' diff --git a/src/Resolver/PharInvocationResolver.php b/src/Resolver/PharInvocationResolver.php index 1d506f2..10bfa80 100644 --- a/src/Resolver/PharInvocationResolver.php +++ b/src/Resolver/PharInvocationResolver.php @@ -33,6 +33,13 @@ class PharInvocationResolver implements Resolvable 'require_once' ]; + /** + * Contains resolved base names in order to reduce file IO. + * + * @var string[] + */ + private $baseNames = []; + /** * Resolves PharInvocation value object (baseName and optional alias). * @@ -56,21 +63,38 @@ public function resolve(string $path, int $flags = null) if ($hasPharPrefix && $flags & static::RESOLVE_ALIAS) { $invocation = $this->findByAlias($path); - if ($invocation !== null && $this->assertInternalInvocation($invocation, $flags)) { + if ($invocation !== null) { return $invocation; - } elseif ($invocation !== null) { - return null; } } - $baseName = Helper::determineBaseFile($path); + $baseName = $this->resolveBaseName($path, $flags); if ($baseName === null) { return null; } if ($flags & static::RESOLVE_REALPATH) { - $baseName = realpath($baseName); + $baseName = $this->baseNames[$baseName]; } + + return $this->retrieveInvocation($baseName, $flags); + } + + /** + * Retrieves PharInvocation, either existing in collection or created on demand + * with resolving a potential alias name used in the according Phar archive. + * + * @param string $baseName + * @param int $flags + * @return PharInvocation + */ + private function retrieveInvocation(string $baseName, int $flags): PharInvocation + { + $invocation = $this->findByBaseName($baseName); + if ($invocation !== null) { + return $invocation; + } + if ($flags & static::RESOLVE_ALIAS) { $alias = (new Reader($baseName))->resolveContainer()->getAlias(); } else { @@ -82,53 +106,128 @@ public function resolve(string $path, int $flags = null) /** * @param string $path - * @return null|PharInvocation + * @param int $flags + * @return null|string */ - private function findByAlias(string $path) + private function resolveBaseName(string $path, int $flags) { - $normalizedPath = Helper::normalizePath($path); - $possibleAlias = strstr($normalizedPath, '/', true); - if (empty($possibleAlias)) { + $baseName = $this->findInBaseNames($path); + if ($baseName !== null) { + return $baseName; + } + + $baseName = Helper::determineBaseFile($path); + if ($baseName !== null) { + $this->addBaseName($baseName); + return $baseName; + } + + $possibleAlias = $this->resolvePossibleAlias($path); + if (!($flags & static::RESOLVE_ALIAS) || $possibleAlias === null) { return null; } + + $trace = debug_backtrace(); + foreach ($trace as $item) { + if (!isset($item['function']) || !isset($item['args'][0]) + || !in_array($item['function'], $this->invocationFunctionNames, true)) { + continue; + } + $currentPath = $item['args'][0]; + if (Helper::hasPharPrefix($currentPath)) { + continue; + } + $currentBaseName = Helper::determineBaseFile($currentPath); + if ($currentBaseName === null) { + continue; + } + // ensure the possible alias name (how we have been called initially) matches + // the resolved alias name that was retrieved by the current possible base name + $currentAlias = (new Reader($currentBaseName))->resolveContainer()->getAlias(); + if ($currentAlias !== $possibleAlias) { + continue; + } + $this->addBaseName($currentBaseName); + return $currentBaseName; + } + + return null; + } + + /** + * @param string $path + * @return null|string + */ + private function resolvePossibleAlias(string $path) + { + $normalizedPath = Helper::normalizePath($path); + return strstr($normalizedPath, '/', true) ?: null; + } + + /** + * @param string $baseName + * @return null|PharInvocation + */ + private function findByBaseName(string $baseName) + { return Manager::instance()->getCollection()->findByCallback( - function (PharInvocation $candidate) use ($possibleAlias) { - return $candidate->getAlias() === $possibleAlias; + function (PharInvocation $candidate) use ($baseName) { + return $candidate->getBaseName() === $baseName; }, true ); } /** - * @param PharInvocation $invocation - * @param int $flags - * @return bool - * @experimental + * @param string $path + * @return null|string */ - private function assertInternalInvocation(PharInvocation $invocation, int $flags): bool + private function findInBaseNames(string $path) { - if (!($flags & static::ASSERT_INTERNAL_INVOCATION)) { - return true; + // return directly if the resolved base name was submitted + if (in_array($path, $this->baseNames, true)) { + return $path; } - $trace = debug_backtrace(0); - $firstIndex = count($trace) - 1; - // initial invocation, most probably a CLI tool - if (($trace[$firstIndex]['file'] ?? null) === $invocation->getBaseName()) { - return true; - } - // otherwise search for include/require invocations - foreach ($trace as $item) { - if (!isset($item['function']) || !isset($item['args'][0])) { - continue; - } - if ($item['args'][0] === $invocation->getBaseName() - && in_array($item['function'], $this->invocationFunctionNames, true) - ) { - return true; + $parts = explode('/', Helper::normalizePath($path)); + + while (count($parts)) { + $currentPath = implode('/', $parts); + if (isset($this->baseNames[$currentPath])) { + return $currentPath; } + array_pop($parts); + } + + return null; + } + + /** + * @param string $baseName + */ + private function addBaseName(string $baseName) + { + if (isset($this->baseNames[$baseName])) { + return; } + $this->baseNames[$baseName] = realpath($baseName); + } - return false; + /** + * @param string $path + * @return null|PharInvocation + */ + private function findByAlias(string $path) + { + $possibleAlias = $this->resolvePossibleAlias($path); + if ($possibleAlias === null) { + return null; + } + return Manager::instance()->getCollection()->findByCallback( + function (PharInvocation $candidate) use ($possibleAlias) { + return $candidate->getAlias() === $possibleAlias; + }, + true + ); } } diff --git a/tests/Functional/Fixtures/Existing/.gitkeep b/tests/Functional/Fixtures/Existing/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/Functional/HelperTest.php b/tests/Functional/HelperTest.php index 138f2fa..d4e6539 100644 --- a/tests/Functional/HelperTest.php +++ b/tests/Functional/HelperTest.php @@ -36,12 +36,16 @@ public function baseFileIsResolvedDataProvider(): array '{DIR}/bundle.phar' ], [ - 'phar://{DIR}/other/../bundle.phar/path/../other/content.txt', - '{DIR}/bundle.phar' + 'phar://{DIR}/Existing/../bundle.phar/path/../other/content.txt', + '{DIR}/Existing/../bundle.phar' ], [ 'phar://{DIR}/../Fixtures/bundle.phar', - '{DIR}/bundle.phar' + '{DIR}/../Fixtures/bundle.phar' + ], + [ + 'phar://{DIR}/NotExisting/../bundle.phar/path/../other/content.txt', + null ], [ 'phar://{DIR}/not-existing.phar/path/../other/content.txt', diff --git a/tests/Functional/Interceptor/AbstractTestCase.php b/tests/Functional/Interceptor/AbstractTestCase.php index e25100d..e16be5f 100644 --- a/tests/Functional/Interceptor/AbstractTestCase.php +++ b/tests/Functional/Interceptor/AbstractTestCase.php @@ -454,23 +454,6 @@ public function streamOpenAllowsInvocationForIncludeOnAliasedPhar(string $allowe static::assertNotFalse($result); } - /** - * @param string $allowedPath - * - * @test - * @dataProvider allowedPathsDataProvider - */ - public function streamOpenDeniesInvocationForAliasedIncludeOutsideAliasedPhar(string $allowedPath) - { - // used to trigger registration of Phar alias - include('phar://' . $allowedPath . '/Classes/Domain/Model/DemoModel.php'); - - self::expectException(Exception::class); - self::expectExceptionCode(static::EXPECTED_EXCEPTION_CODE); - // using Phar alias outside(!) of according Phar archive - include('phar://bndl.phar/Classes/Domain/Model/DemoModel.php'); - } - /** * @param string $deniedPath * diff --git a/tests/Functional/Interceptor/ConjunctionInterceptorTest.php b/tests/Functional/Interceptor/ConjunctionInterceptorTest.php index cabcd71..5531e02 100644 --- a/tests/Functional/Interceptor/ConjunctionInterceptorTest.php +++ b/tests/Functional/Interceptor/ConjunctionInterceptorTest.php @@ -34,7 +34,7 @@ class ConjunctionInterceptorTest extends AbstractTestCase */ protected $allowedAliasedPaths = [ __DIR__ . '/../Fixtures/geoip2.phar', - // __DIR__ . '/../Fixtures/alias-no-path.phar', + __DIR__ . '/../Fixtures/alias-no-path.phar', __DIR__ . '/../Fixtures/alias-with-path.phar', ]; @@ -91,15 +91,13 @@ public function isFileSystemInvocationAcceptableDataProvider(): array $fixturePath = __DIR__ . '/../Fixtures'; return [ - /* 'include phar' => [ $fixturePath . '/geoip2.phar', - [Helper::class . '::determineBaseFile' => 1, Reader::class . '->resolveContainer' => 2] + [Helper::class . '::determineBaseFile' => 1, Reader::class . '->resolveContainer' => 19] ], - */ 'include autoloader' => [ 'phar://' . $fixturePath . '/geoip2.phar/vendor/autoload.php', - [Helper::class . '::determineBaseFile' => 45, Reader::class . '->resolveContainer' => 60] + [Helper::class . '::determineBaseFile' => 1, Reader::class . '->resolveContainer' => 18] ], ]; } diff --git a/tests/Functional/Interceptor/PharExtensionInterceptorTest.php b/tests/Functional/Interceptor/PharExtensionInterceptorTest.php index 215d005..d73872e 100644 --- a/tests/Functional/Interceptor/PharExtensionInterceptorTest.php +++ b/tests/Functional/Interceptor/PharExtensionInterceptorTest.php @@ -32,7 +32,7 @@ class PharExtensionInterceptorTest extends AbstractTestCase */ protected $allowedAliasedPaths = [ __DIR__ . '/../Fixtures/geoip2.phar', - // __DIR__ . '/../Fixtures/alias-no-path.phar', + __DIR__ . '/../Fixtures/alias-no-path.phar', __DIR__ . '/../Fixtures/alias-with-path.phar', ]; @@ -130,15 +130,13 @@ public function isFileSystemInvocationAcceptableDataProvider(): array $fixturePath = __DIR__ . '/../Fixtures'; return [ - /* 'include phar' => [ $fixturePath . '/geoip2.phar', [Helper::class . '::determineBaseFile' => 1, Reader::class . '->resolveContainer' => 2] ], - */ 'include autoloader' => [ 'phar://' . $fixturePath . '/geoip2.phar/vendor/autoload.php', - [Helper::class . '::determineBaseFile' => 30, Reader::class . '->resolveContainer' => 30] + [Helper::class . '::determineBaseFile' => 1, Reader::class . '->resolveContainer' => 2] ], ]; } diff --git a/tests/Functional/Interceptor/PharMetaDataInterceptorTest.php b/tests/Functional/Interceptor/PharMetaDataInterceptorTest.php index 1350cea..e6f1238 100644 --- a/tests/Functional/Interceptor/PharMetaDataInterceptorTest.php +++ b/tests/Functional/Interceptor/PharMetaDataInterceptorTest.php @@ -33,7 +33,7 @@ class PharMetaDataInterceptorTest extends AbstractTestCase */ protected $allowedAliasedPaths = [ __DIR__ . '/../Fixtures/geoip2.phar', - // __DIR__ . '/../Fixtures/alias-no-path.phar', + __DIR__ . '/../Fixtures/alias-no-path.phar', __DIR__ . '/../Fixtures/alias-with-path.phar', ]; @@ -86,15 +86,13 @@ public function isFileSystemInvocationAcceptableDataProvider(): array $fixturePath = __DIR__ . '/../Fixtures'; return [ - /* 'include phar' => [ $fixturePath . '/geoip2.phar', - [Helper::class . '::determineBaseFile' => 1, Reader::class . '->resolveContainer' => 2] + [Helper::class . '::determineBaseFile' => 1, Reader::class . '->resolveContainer' => 18] ], - */ 'include autoloader' => [ 'phar://' . $fixturePath . '/geoip2.phar/vendor/autoload.php', - [Helper::class . '::determineBaseFile' => 30, Reader::class . '->resolveContainer' => 45] + [Helper::class . '::determineBaseFile' => 1, Reader::class . '->resolveContainer' => 17] ], ]; } diff --git a/tests/Unit/HelperTest.php b/tests/Unit/HelperTest.php index d0473e6..bc174cc 100644 --- a/tests/Unit/HelperTest.php +++ b/tests/Unit/HelperTest.php @@ -57,24 +57,17 @@ public function pharPrefixIsRemoved(string $path, string $expectation) public function pathIsNormalizedDataProvider(): array { $dataSet = [ - ['.', ''], - ['..', ''], - ['../x', 'x'], - ['./././x', 'x'], - ['./.././../x', 'x'], - ['a/../x', 'x'], - ['a/b/../../x', 'x'], - ['/a/b/../../x', '/x'], - ['c:\\a\\b\..\..\x', 'c:/x'], - ['phar://../x', 'x'], - ['phar://../x/file.phar', 'x/file.phar'], - ['phar:///../x/file.phar', '/x/file.phar'], - ['phar://a/b/../../x', 'x'], - ['phar:///a/b/../../x', '/x'], - ['phar://a/b/../../x/file.phar', 'x/file.phar'], - ['phar:///a/b/../../x/file.phar', '/x/file.phar'], - ['phar://c:\\a\\b\\..\\..\\x\\file.phar', 'c:/x/file.phar'], - [' phar:///a/b/../../x/file.phar ', '/x/file.phar'], + ['.', '.'], + ['..', '..'], + ['./x', './x'], + ['../x', '../x'], + ['c:\\a\\b\..\..\x', 'c:/a/b/../../x'], + ['phar://../x', '../x'], + ['phar:///../x', '/../x'], + ['phar://c:\\a\\b\..\..\x', 'c:/a/b/../../x'], + [' phar://../x ', '../x'], + [' phar:///../x ', '/../x'], + [' phar://c:\\a\\b\..\..\x ', 'c:/a/b/../../x'], ]; return array_merge($this->pharPrefixIsRemovedDataProvider(), $dataSet);