1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\File;
4:
5: use Nette\Utils\Strings;
6: use PHPStan\DependencyInjection\AutowiredParameter;
7: use PHPStan\DependencyInjection\AutowiredService;
8: use function array_pop;
9: use function explode;
10: use function implode;
11: use function ltrim;
12: use function preg_match;
13: use function rtrim;
14: use function str_ends_with;
15: use function str_replace;
16: use function str_starts_with;
17: use function strlen;
18: use function strtolower;
19: use function substr;
20: use function trim;
21: use const DIRECTORY_SEPARATOR;
22:
23: #[AutowiredService]
24: final class FileHelper
25: {
26:
27: /** @var array<string, array<string, string>> */
28: private static array $normalizedPathsCache = [];
29:
30: private string $workingDirectory;
31:
32: public function __construct(
33: #[AutowiredParameter(ref: '%currentWorkingDirectory%')]
34: string $workingDirectory,
35: )
36: {
37: $this->workingDirectory = $this->normalizePath($workingDirectory);
38: }
39:
40: public function getWorkingDirectory(): string
41: {
42: return $this->workingDirectory;
43: }
44:
45: /** @api */
46: public function absolutizePath(string $path): string
47: {
48: if (DIRECTORY_SEPARATOR === '/') {
49: if (str_starts_with($path, '/')) {
50: return $path;
51: }
52: } elseif (substr($path, 1, 1) === ':') {
53: return $path;
54: }
55:
56: if (preg_match('~^[a-z0-9+\-.]+://~i', $path) === 1) {
57: return $path;
58: }
59:
60: return rtrim($this->getWorkingDirectory(), '/\\') . DIRECTORY_SEPARATOR . ltrim($path, '/\\');
61: }
62:
63: /** @api */
64: public function normalizePath(string $originalPath, string $directorySeparator = DIRECTORY_SEPARATOR): string
65: {
66: $cached = self::$normalizedPathsCache[$originalPath][$directorySeparator] ?? null;
67: if ($cached !== null) {
68: return $cached;
69: }
70:
71: $isLocalPath = false;
72: if ($originalPath !== '') {
73: if ($originalPath[0] === '/') {
74: $isLocalPath = true;
75: } elseif (strlen($originalPath) >= 3 && $originalPath[1] === ':' && $originalPath[2] === '\\') { // e.g. C:\
76: $isLocalPath = true;
77: }
78: }
79:
80: $matches = null;
81: if (!$isLocalPath) {
82: $matches = Strings::match($originalPath, '~^([a-z0-9+\-.]+)://(.+)$~is');
83: }
84:
85: if ($matches !== null) {
86: [, $scheme, $path] = $matches;
87: $scheme = strtolower($scheme);
88: } else {
89: $scheme = null;
90: $path = $originalPath;
91: }
92:
93: $path = str_replace(['\\', '//', '///', '////'], '/', $path);
94:
95: $pathRoot = str_starts_with($path, '/') ? $directorySeparator : '';
96: $pathParts = explode('/', trim($path, '/'));
97:
98: $normalizedPathParts = [];
99: foreach ($pathParts as $pathPart) {
100: if ($pathPart === '.') {
101: continue;
102: }
103: if ($pathPart === '..') {
104: $removedPart = array_pop($normalizedPathParts);
105: if ($scheme === 'phar' && $removedPart !== null && str_ends_with($removedPart, '.phar')) {
106: $scheme = null;
107: }
108: } else {
109: $normalizedPathParts[] = $pathPart;
110: }
111: }
112:
113: return self::$normalizedPathsCache[$originalPath][$directorySeparator] = ($scheme !== null ? $scheme . '://' : '') . $pathRoot . implode($directorySeparator, $normalizedPathParts);
114: }
115:
116: }
117: