PluginProbe ʕ •ᴥ•ʔ
10Web Booster – Website speed optimization, Cache & Page Speed optimizer / trunk
10Web Booster – Website speed optimization, Cache & Page Speed optimizer vtrunk
2.33.0 2.30.5 2.30.7 2.30.9 2.31.10 2.31.8 2.32.11 2.32.21 2.32.3 2.32.4 2.32.7 2.6.31 2.6.40 2.6.42 2.6.7 2.7.37 2.7.44 2.7.47 2.8.18 2.8.19 2.8.32 2.8.34 2.8.35 2.9.23 2.9.24 2.9.25 2.9.27 v2.27.4 trunk 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.17 2.0.18 2.0.21 2.0.22 2.0.25 2.0.26 2.0.27 2.0.3 2.0.7 2.0.9 2.10.46 2.10.65 2.10.66 2.10.68 2.11.41 2.11.42 2.11.43 2.12.15 2.12.21 2.12.22 2.12.23 2.12.26 2.13.37 2.13.40 2.13.41 2.13.42 2.13.44 2.13.45 2.13.47 2.14.49 2.14.50 2.15.18 2.17.21 2.17.23 2.18.17 2.19.44 2.19.45 2.19.46 2.19.49 2.2.12 2.2.15 2.2.16 2.2.18 2.2.8 2.20.31 2.20.32 2.20.33 2.21.11 2.21.12 2.21.16 2.21.25 2.22.32 2.23.13 2.23.15 2.23.16 2.23.18 2.24.12 2.24.14 2.24.18 2.25.14 2.26.6 2.28.10 2.28.13 2.28.14 2.28.7 2.29.1 2.29.2 2.29.3 2.3.0 2.3.1 2.3.2 2.3.3 2.30.18
tenweb-speed-optimizer / vendor / symfony / filesystem / Path.php
tenweb-speed-optimizer / vendor / symfony / filesystem Last commit date
Exception 2 years ago CHANGELOG.md 2 years ago Filesystem.php 1 year ago LICENSE 2 years ago Path.php 2 years ago README.md 4 years ago composer.json 2 years ago
Path.php
820 lines
1 <?php
2
3 /*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Symfony\Component\Filesystem;
13
14 use Symfony\Component\Filesystem\Exception\InvalidArgumentException;
15 use Symfony\Component\Filesystem\Exception\RuntimeException;
16
17 /**
18 * Contains utility methods for handling path strings.
19 *
20 * The methods in this class are able to deal with both UNIX and Windows paths
21 * with both forward and backward slashes. All methods return normalized parts
22 * containing only forward slashes and no excess "." and ".." segments.
23 *
24 * @author Bernhard Schussek <bschussek@gmail.com>
25 * @author Thomas Schulz <mail@king2500.net>
26 * @author Théo Fidry <theo.fidry@gmail.com>
27 */
28 final class Path
29 {
30 /**
31 * The number of buffer entries that triggers a cleanup operation.
32 */
33 private const CLEANUP_THRESHOLD = 1250;
34
35 /**
36 * The buffer size after the cleanup operation.
37 */
38 private const CLEANUP_SIZE = 1000;
39
40 /**
41 * Buffers input/output of {@link canonicalize()}.
42 *
43 * @var array<string, string>
44 */
45 private static $buffer = [];
46
47 /**
48 * @var int
49 */
50 private static $bufferSize = 0;
51
52 /**
53 * Canonicalizes the given path.
54 *
55 * During normalization, all slashes are replaced by forward slashes ("/").
56 * Furthermore, all "." and ".." segments are removed as far as possible.
57 * ".." segments at the beginning of relative paths are not removed.
58 *
59 * ```php
60 * echo Path::canonicalize("\symfony\puli\..\css\style.css");
61 * // => /symfony/css/style.css
62 *
63 * echo Path::canonicalize("../css/./style.css");
64 * // => ../css/style.css
65 * ```
66 *
67 * This method is able to deal with both UNIX and Windows paths.
68 */
69 public static function canonicalize(string $path): string
70 {
71 if ('' === $path) {
72 return '';
73 }
74
75 // This method is called by many other methods in this class. Buffer
76 // the canonicalized paths to make up for the severe performance
77 // decrease.
78 if (isset(self::$buffer[$path])) {
79 return self::$buffer[$path];
80 }
81
82 // Replace "~" with user's home directory.
83 if ('~' === $path[0]) {
84 $path = self::getHomeDirectory().substr($path, 1);
85 }
86
87 $path = self::normalize($path);
88
89 [$root, $pathWithoutRoot] = self::split($path);
90
91 $canonicalParts = self::findCanonicalParts($root, $pathWithoutRoot);
92
93 // Add the root directory again
94 self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts);
95 ++self::$bufferSize;
96
97 // Clean up regularly to prevent memory leaks
98 if (self::$bufferSize > self::CLEANUP_THRESHOLD) {
99 self::$buffer = \array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true);
100 self::$bufferSize = self::CLEANUP_SIZE;
101 }
102
103 return $canonicalPath;
104 }
105
106 /**
107 * Normalizes the given path.
108 *
109 * During normalization, all slashes are replaced by forward slashes ("/").
110 * Contrary to {@link canonicalize()}, this method does not remove invalid
111 * or dot path segments. Consequently, it is much more efficient and should
112 * be used whenever the given path is known to be a valid, absolute system
113 * path.
114 *
115 * This method is able to deal with both UNIX and Windows paths.
116 */
117 public static function normalize(string $path): string
118 {
119 return str_replace('\\', '/', $path);
120 }
121
122 /**
123 * Returns the directory part of the path.
124 *
125 * This method is similar to PHP's dirname(), but handles various cases
126 * where dirname() returns a weird result:
127 *
128 * - dirname() does not accept backslashes on UNIX
129 * - dirname("C:/symfony") returns "C:", not "C:/"
130 * - dirname("C:/") returns ".", not "C:/"
131 * - dirname("C:") returns ".", not "C:/"
132 * - dirname("symfony") returns ".", not ""
133 * - dirname() does not canonicalize the result
134 *
135 * This method fixes these shortcomings and behaves like dirname()
136 * otherwise.
137 *
138 * The result is a canonical path.
139 *
140 * @return string The canonical directory part. Returns the root directory
141 * if the root directory is passed. Returns an empty string
142 * if a relative path is passed that contains no slashes.
143 * Returns an empty string if an empty string is passed.
144 */
145 public static function getDirectory(string $path): string
146 {
147 if ('' === $path) {
148 return '';
149 }
150
151 $path = self::canonicalize($path);
152
153 // Maintain scheme
154 if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
155 $scheme = substr($path, 0, $schemeSeparatorPosition + 3);
156 $path = substr($path, $schemeSeparatorPosition + 3);
157 } else {
158 $scheme = '';
159 }
160
161 if (false === $dirSeparatorPosition = strrpos($path, '/')) {
162 return '';
163 }
164
165 // Directory equals root directory "/"
166 if (0 === $dirSeparatorPosition) {
167 return $scheme.'/';
168 }
169
170 // Directory equals Windows root "C:/"
171 if (2 === $dirSeparatorPosition && ctype_alpha($path[0]) && ':' === $path[1]) {
172 return $scheme.substr($path, 0, 3);
173 }
174
175 return $scheme.substr($path, 0, $dirSeparatorPosition);
176 }
177
178 /**
179 * Returns canonical path of the user's home directory.
180 *
181 * Supported operating systems:
182 *
183 * - UNIX
184 * - Windows8 and upper
185 *
186 * If your operating system or environment isn't supported, an exception is thrown.
187 *
188 * The result is a canonical path.
189 *
190 * @throws RuntimeException If your operating system or environment isn't supported
191 */
192 public static function getHomeDirectory(): string
193 {
194 // For UNIX support
195 if (getenv('HOME')) {
196 return self::canonicalize(getenv('HOME'));
197 }
198
199 // For >= Windows8 support
200 if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) {
201 return self::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH'));
202 }
203
204 throw new RuntimeException("Cannot find the home directory path: Your environment or operating system isn't supported.");
205 }
206
207 /**
208 * Returns the root directory of a path.
209 *
210 * The result is a canonical path.
211 *
212 * @return string The canonical root directory. Returns an empty string if
213 * the given path is relative or empty.
214 */
215 public static function getRoot(string $path): string
216 {
217 if ('' === $path) {
218 return '';
219 }
220
221 // Maintain scheme
222 if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
223 $scheme = substr($path, 0, $schemeSeparatorPosition + 3);
224 $path = substr($path, $schemeSeparatorPosition + 3);
225 } else {
226 $scheme = '';
227 }
228
229 $firstCharacter = $path[0];
230
231 // UNIX root "/" or "\" (Windows style)
232 if ('/' === $firstCharacter || '\\' === $firstCharacter) {
233 return $scheme.'/';
234 }
235
236 $length = \strlen($path);
237
238 // Windows root
239 if ($length > 1 && ':' === $path[1] && ctype_alpha($firstCharacter)) {
240 // Special case: "C:"
241 if (2 === $length) {
242 return $scheme.$path.'/';
243 }
244
245 // Normal case: "C:/ or "C:\"
246 if ('/' === $path[2] || '\\' === $path[2]) {
247 return $scheme.$firstCharacter.$path[1].'/';
248 }
249 }
250
251 return '';
252 }
253
254 /**
255 * Returns the file name without the extension from a file path.
256 *
257 * @param string|null $extension if specified, only that extension is cut
258 * off (may contain leading dot)
259 */
260 public static function getFilenameWithoutExtension(string $path, ?string $extension = null): string
261 {
262 if ('' === $path) {
263 return '';
264 }
265
266 if (null !== $extension) {
267 // remove extension and trailing dot
268 return rtrim(basename($path, $extension), '.');
269 }
270
271 return pathinfo($path, \PATHINFO_FILENAME);
272 }
273
274 /**
275 * Returns the extension from a file path (without leading dot).
276 *
277 * @param bool $forceLowerCase forces the extension to be lower-case
278 */
279 public static function getExtension(string $path, bool $forceLowerCase = false): string
280 {
281 if ('' === $path) {
282 return '';
283 }
284
285 $extension = pathinfo($path, \PATHINFO_EXTENSION);
286
287 if ($forceLowerCase) {
288 $extension = self::toLower($extension);
289 }
290
291 return $extension;
292 }
293
294 /**
295 * Returns whether the path has an (or the specified) extension.
296 *
297 * @param string $path the path string
298 * @param string|string[]|null $extensions if null or not provided, checks if
299 * an extension exists, otherwise
300 * checks for the specified extension
301 * or array of extensions (with or
302 * without leading dot)
303 * @param bool $ignoreCase whether to ignore case-sensitivity
304 */
305 public static function hasExtension(string $path, $extensions = null, bool $ignoreCase = false): bool
306 {
307 if ('' === $path) {
308 return false;
309 }
310
311 $actualExtension = self::getExtension($path, $ignoreCase);
312
313 // Only check if path has any extension
314 if ([] === $extensions || null === $extensions) {
315 return '' !== $actualExtension;
316 }
317
318 if (\is_string($extensions)) {
319 $extensions = [$extensions];
320 }
321
322 foreach ($extensions as $key => $extension) {
323 if ($ignoreCase) {
324 $extension = self::toLower($extension);
325 }
326
327 // remove leading '.' in extensions array
328 $extensions[$key] = ltrim($extension, '.');
329 }
330
331 return \in_array($actualExtension, $extensions, true);
332 }
333
334 /**
335 * Changes the extension of a path string.
336 *
337 * @param string $path The path string with filename.ext to change.
338 * @param string $extension new extension (with or without leading dot)
339 *
340 * @return string the path string with new file extension
341 */
342 public static function changeExtension(string $path, string $extension): string
343 {
344 if ('' === $path) {
345 return '';
346 }
347
348 $actualExtension = self::getExtension($path);
349 $extension = ltrim($extension, '.');
350
351 // No extension for paths
352 if ('/' === substr($path, -1)) {
353 return $path;
354 }
355
356 // No actual extension in path
357 if (empty($actualExtension)) {
358 return $path.('.' === substr($path, -1) ? '' : '.').$extension;
359 }
360
361 return substr($path, 0, -\strlen($actualExtension)).$extension;
362 }
363
364 public static function isAbsolute(string $path): bool
365 {
366 if ('' === $path) {
367 return false;
368 }
369
370 // Strip scheme
371 if (false !== ($schemeSeparatorPosition = strpos($path, '://')) && 1 !== $schemeSeparatorPosition) {
372 $path = substr($path, $schemeSeparatorPosition + 3);
373 }
374
375 $firstCharacter = $path[0];
376
377 // UNIX root "/" or "\" (Windows style)
378 if ('/' === $firstCharacter || '\\' === $firstCharacter) {
379 return true;
380 }
381
382 // Windows root
383 if (\strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) {
384 // Special case: "C:"
385 if (2 === \strlen($path)) {
386 return true;
387 }
388
389 // Normal case: "C:/ or "C:\"
390 if ('/' === $path[2] || '\\' === $path[2]) {
391 return true;
392 }
393 }
394
395 return false;
396 }
397
398 public static function isRelative(string $path): bool
399 {
400 return !self::isAbsolute($path);
401 }
402
403 /**
404 * Turns a relative path into an absolute path in canonical form.
405 *
406 * Usually, the relative path is appended to the given base path. Dot
407 * segments ("." and "..") are removed/collapsed and all slashes turned
408 * into forward slashes.
409 *
410 * ```php
411 * echo Path::makeAbsolute("../style.css", "/symfony/puli/css");
412 * // => /symfony/puli/style.css
413 * ```
414 *
415 * If an absolute path is passed, that path is returned unless its root
416 * directory is different than the one of the base path. In that case, an
417 * exception is thrown.
418 *
419 * ```php
420 * Path::makeAbsolute("/style.css", "/symfony/puli/css");
421 * // => /style.css
422 *
423 * Path::makeAbsolute("C:/style.css", "C:/symfony/puli/css");
424 * // => C:/style.css
425 *
426 * Path::makeAbsolute("C:/style.css", "/symfony/puli/css");
427 * // InvalidArgumentException
428 * ```
429 *
430 * If the base path is not an absolute path, an exception is thrown.
431 *
432 * The result is a canonical path.
433 *
434 * @param string $basePath an absolute base path
435 *
436 * @throws InvalidArgumentException if the base path is not absolute or if
437 * the given path is an absolute path with
438 * a different root than the base path
439 */
440 public static function makeAbsolute(string $path, string $basePath): string
441 {
442 if ('' === $basePath) {
443 throw new InvalidArgumentException(sprintf('The base path must be a non-empty string. Got: "%s".', $basePath));
444 }
445
446 if (!self::isAbsolute($basePath)) {
447 throw new InvalidArgumentException(sprintf('The base path "%s" is not an absolute path.', $basePath));
448 }
449
450 if (self::isAbsolute($path)) {
451 return self::canonicalize($path);
452 }
453
454 if (false !== $schemeSeparatorPosition = strpos($basePath, '://')) {
455 $scheme = substr($basePath, 0, $schemeSeparatorPosition + 3);
456 $basePath = substr($basePath, $schemeSeparatorPosition + 3);
457 } else {
458 $scheme = '';
459 }
460
461 return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path);
462 }
463
464 /**
465 * Turns a path into a relative path.
466 *
467 * The relative path is created relative to the given base path:
468 *
469 * ```php
470 * echo Path::makeRelative("/symfony/style.css", "/symfony/puli");
471 * // => ../style.css
472 * ```
473 *
474 * If a relative path is passed and the base path is absolute, the relative
475 * path is returned unchanged:
476 *
477 * ```php
478 * Path::makeRelative("style.css", "/symfony/puli/css");
479 * // => style.css
480 * ```
481 *
482 * If both paths are relative, the relative path is created with the
483 * assumption that both paths are relative to the same directory:
484 *
485 * ```php
486 * Path::makeRelative("style.css", "symfony/puli/css");
487 * // => ../../../style.css
488 * ```
489 *
490 * If both paths are absolute, their root directory must be the same,
491 * otherwise an exception is thrown:
492 *
493 * ```php
494 * Path::makeRelative("C:/symfony/style.css", "/symfony/puli");
495 * // InvalidArgumentException
496 * ```
497 *
498 * If the passed path is absolute, but the base path is not, an exception
499 * is thrown as well:
500 *
501 * ```php
502 * Path::makeRelative("/symfony/style.css", "symfony/puli");
503 * // InvalidArgumentException
504 * ```
505 *
506 * If the base path is not an absolute path, an exception is thrown.
507 *
508 * The result is a canonical path.
509 *
510 * @throws InvalidArgumentException if the base path is not absolute or if
511 * the given path has a different root
512 * than the base path
513 */
514 public static function makeRelative(string $path, string $basePath): string
515 {
516 $path = self::canonicalize($path);
517 $basePath = self::canonicalize($basePath);
518
519 [$root, $relativePath] = self::split($path);
520 [$baseRoot, $relativeBasePath] = self::split($basePath);
521
522 // If the base path is given as absolute path and the path is already
523 // relative, consider it to be relative to the given absolute path
524 // already
525 if ('' === $root && '' !== $baseRoot) {
526 // If base path is already in its root
527 if ('' === $relativeBasePath) {
528 $relativePath = ltrim($relativePath, './\\');
529 }
530
531 return $relativePath;
532 }
533
534 // If the passed path is absolute, but the base path is not, we
535 // cannot generate a relative path
536 if ('' !== $root && '' === $baseRoot) {
537 throw new InvalidArgumentException(sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath));
538 }
539
540 // Fail if the roots of the two paths are different
541 if ($baseRoot && $root !== $baseRoot) {
542 throw new InvalidArgumentException(sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot));
543 }
544
545 if ('' === $relativeBasePath) {
546 return $relativePath;
547 }
548
549 // Build a "../../" prefix with as many "../" parts as necessary
550 $parts = explode('/', $relativePath);
551 $baseParts = explode('/', $relativeBasePath);
552 $dotDotPrefix = '';
553
554 // Once we found a non-matching part in the prefix, we need to add
555 // "../" parts for all remaining parts
556 $match = true;
557
558 foreach ($baseParts as $index => $basePart) {
559 if ($match && isset($parts[$index]) && $basePart === $parts[$index]) {
560 unset($parts[$index]);
561
562 continue;
563 }
564
565 $match = false;
566 $dotDotPrefix .= '../';
567 }
568
569 return rtrim($dotDotPrefix.implode('/', $parts), '/');
570 }
571
572 /**
573 * Returns whether the given path is on the local filesystem.
574 */
575 public static function isLocal(string $path): bool
576 {
577 return '' !== $path && false === strpos($path, '://');
578 }
579
580 /**
581 * Returns the longest common base path in canonical form of a set of paths or
582 * `null` if the paths are on different Windows partitions.
583 *
584 * Dot segments ("." and "..") are removed/collapsed and all slashes turned
585 * into forward slashes.
586 *
587 * ```php
588 * $basePath = Path::getLongestCommonBasePath(
589 * '/symfony/css/style.css',
590 * '/symfony/css/..'
591 * );
592 * // => /symfony
593 * ```
594 *
595 * The root is returned if no common base path can be found:
596 *
597 * ```php
598 * $basePath = Path::getLongestCommonBasePath(
599 * '/symfony/css/style.css',
600 * '/puli/css/..'
601 * );
602 * // => /
603 * ```
604 *
605 * If the paths are located on different Windows partitions, `null` is
606 * returned.
607 *
608 * ```php
609 * $basePath = Path::getLongestCommonBasePath(
610 * 'C:/symfony/css/style.css',
611 * 'D:/symfony/css/..'
612 * );
613 * // => null
614 * ```
615 */
616 public static function getLongestCommonBasePath(string ...$paths): ?string
617 {
618 [$bpRoot, $basePath] = self::split(self::canonicalize(reset($paths)));
619
620 for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) {
621 [$root, $path] = self::split(self::canonicalize(current($paths)));
622
623 // If we deal with different roots (e.g. C:/ vs. D:/), it's time
624 // to quit
625 if ($root !== $bpRoot) {
626 return null;
627 }
628
629 // Make the base path shorter until it fits into path
630 while (true) {
631 if ('.' === $basePath) {
632 // No more base paths
633 $basePath = '';
634
635 // next path
636 continue 2;
637 }
638
639 // Prevent false positives for common prefixes
640 // see isBasePath()
641 if (0 === strpos($path.'/', $basePath.'/')) {
642 // next path
643 continue 2;
644 }
645
646 $basePath = \dirname($basePath);
647 }
648 }
649
650 return $bpRoot.$basePath;
651 }
652
653 /**
654 * Joins two or more path strings into a canonical path.
655 */
656 public static function join(string ...$paths): string
657 {
658 $finalPath = null;
659 $wasScheme = false;
660
661 foreach ($paths as $path) {
662 if ('' === $path) {
663 continue;
664 }
665
666 if (null === $finalPath) {
667 // For first part we keep slashes, like '/top', 'C:\' or 'phar://'
668 $finalPath = $path;
669 $wasScheme = (false !== strpos($path, '://'));
670 continue;
671 }
672
673 // Only add slash if previous part didn't end with '/' or '\'
674 if (!\in_array(substr($finalPath, -1), ['/', '\\'])) {
675 $finalPath .= '/';
676 }
677
678 // If first part included a scheme like 'phar://' we allow \current part to start with '/', otherwise trim
679 $finalPath .= $wasScheme ? $path : ltrim($path, '/');
680 $wasScheme = false;
681 }
682
683 if (null === $finalPath) {
684 return '';
685 }
686
687 return self::canonicalize($finalPath);
688 }
689
690 /**
691 * Returns whether a path is a base path of another path.
692 *
693 * Dot segments ("." and "..") are removed/collapsed and all slashes turned
694 * into forward slashes.
695 *
696 * ```php
697 * Path::isBasePath('/symfony', '/symfony/css');
698 * // => true
699 *
700 * Path::isBasePath('/symfony', '/symfony');
701 * // => true
702 *
703 * Path::isBasePath('/symfony', '/symfony/..');
704 * // => false
705 *
706 * Path::isBasePath('/symfony', '/puli');
707 * // => false
708 * ```
709 */
710 public static function isBasePath(string $basePath, string $ofPath): bool
711 {
712 $basePath = self::canonicalize($basePath);
713 $ofPath = self::canonicalize($ofPath);
714
715 // Append slashes to prevent false positives when two paths have
716 // a common prefix, for example /base/foo and /base/foobar.
717 // Don't append a slash for the root "/", because then that root
718 // won't be discovered as common prefix ("//" is not a prefix of
719 // "/foobar/").
720 return 0 === strpos($ofPath.'/', rtrim($basePath, '/').'/');
721 }
722
723 /**
724 * @return string[]
725 */
726 private static function findCanonicalParts(string $root, string $pathWithoutRoot): array
727 {
728 $parts = explode('/', $pathWithoutRoot);
729
730 $canonicalParts = [];
731
732 // Collapse "." and "..", if possible
733 foreach ($parts as $part) {
734 if ('.' === $part || '' === $part) {
735 continue;
736 }
737
738 // Collapse ".." with the previous part, if one exists
739 // Don't collapse ".." if the previous part is also ".."
740 if ('..' === $part && \count($canonicalParts) > 0 && '..' !== $canonicalParts[\count($canonicalParts) - 1]) {
741 array_pop($canonicalParts);
742
743 continue;
744 }
745
746 // Only add ".." prefixes for relative paths
747 if ('..' !== $part || '' === $root) {
748 $canonicalParts[] = $part;
749 }
750 }
751
752 return $canonicalParts;
753 }
754
755 /**
756 * Splits a canonical path into its root directory and the remainder.
757 *
758 * If the path has no root directory, an empty root directory will be
759 * returned.
760 *
761 * If the root directory is a Windows style partition, the resulting root
762 * will always contain a trailing slash.
763 *
764 * list ($root, $path) = Path::split("C:/symfony")
765 * // => ["C:/", "symfony"]
766 *
767 * list ($root, $path) = Path::split("C:")
768 * // => ["C:/", ""]
769 *
770 * @return array{string, string} an array with the root directory and the remaining relative path
771 */
772 private static function split(string $path): array
773 {
774 if ('' === $path) {
775 return ['', ''];
776 }
777
778 // Remember scheme as part of the root, if any
779 if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
780 $root = substr($path, 0, $schemeSeparatorPosition + 3);
781 $path = substr($path, $schemeSeparatorPosition + 3);
782 } else {
783 $root = '';
784 }
785
786 $length = \strlen($path);
787
788 // Remove and remember root directory
789 if (0 === strpos($path, '/')) {
790 $root .= '/';
791 $path = $length > 1 ? substr($path, 1) : '';
792 } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
793 if (2 === $length) {
794 // Windows special case: "C:"
795 $root .= $path.'/';
796 $path = '';
797 } elseif ('/' === $path[2]) {
798 // Windows normal case: "C:/"..
799 $root .= substr($path, 0, 3);
800 $path = $length > 3 ? substr($path, 3) : '';
801 }
802 }
803
804 return [$root, $path];
805 }
806
807 private static function toLower(string $string): string
808 {
809 if (false !== $encoding = mb_detect_encoding($string, null, true)) {
810 return mb_strtolower($string, $encoding);
811 }
812
813 return strtolower($string);
814 }
815
816 private function __construct()
817 {
818 }
819 }
820